ki-manager 2.3.0.dev1__py3-none-any.whl → 2.3.0.dev2__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.0.dev1"
3
+ __version__ = "2.3.0.dev2"
@@ -196,7 +196,8 @@ def search_knowledge(
196
196
  query: str,
197
197
  project_root: Optional[str] = None,
198
198
  scope: str = "all",
199
- limit: int = 10,
199
+ limit: int = 5,
200
+ offset: int = 0,
200
201
  k1: float = 1.5,
201
202
  b: float = 0.75,
202
203
  ) -> Dict[str, Any]:
@@ -206,7 +207,8 @@ def search_knowledge(
206
207
  :param query: Search string (e.g. "workspace detection" or "управление зависимостями")
207
208
  :param project_root: Root path of the project. If None, resolved via ki_utils.
208
209
  :param scope: "all", "ki", or "adr".
209
- :param limit: Max number of results to return.
210
+ :param limit: Max number of results to return (default: 5).
211
+ :param offset: Offset index for pagination (default: 0).
210
212
  :param k1: BM25 k1 parameter.
211
213
  :param b: BM25 b parameter.
212
214
  """
@@ -296,10 +298,45 @@ def search_knowledge(
296
298
  })
297
299
 
298
300
  scored_results.sort(key=lambda x: x["score"], reverse=True)
299
- final_results = scored_results[:limit]
301
+ safe_offset = max(0, offset)
302
+ final_results = scored_results[safe_offset : safe_offset + limit]
300
303
 
301
304
  return {
302
305
  "query": query,
303
306
  "total": len(scored_results),
307
+ "offset": safe_offset,
308
+ "limit": limit,
304
309
  "results": final_results,
305
310
  }
311
+
312
+
313
+ def format_search_markdown(search_dict: Dict[str, Any]) -> str:
314
+ """Formats search results into a clean, compact Markdown summary."""
315
+ query = search_dict.get("query", "")
316
+ total = search_dict.get("total", 0)
317
+ results = search_dict.get("results", [])
318
+ offset = search_dict.get("offset", 0)
319
+ limit = search_dict.get("limit", 5)
320
+
321
+ if total == 0 or not results:
322
+ return f'No documentation matching "{query}" found.'
323
+
324
+ start_idx = offset + 1
325
+ end_idx = offset + len(results)
326
+ out = [f'Found {total} result(s) for "{query}" (showing {start_idx}-{end_idx}):\n']
327
+
328
+ for i, r in enumerate(results, start=start_idx):
329
+ doc_type = r.get("type", "ki").upper()
330
+ title = r.get("title", "")
331
+ path = r.get("path", "")
332
+ score = r.get("score", 0.0)
333
+ snippet = r.get("snippet", "").strip()
334
+
335
+ out.append(f"{i}. [{doc_type}] **{title}** (`{path}`, score: {score})")
336
+ if snippet:
337
+ out.append(f" > {snippet}")
338
+
339
+ if end_idx < total:
340
+ out.append(f"\n*Showing {end_idx} of {total} results. Call ki_search(query='{query}', offset={end_idx}) to view more.*")
341
+
342
+ return "\n".join(out)
ki_manager/server.py CHANGED
@@ -204,8 +204,8 @@ MCP_TOOLS = [
204
204
  "- 'knowledge-items' \u2014 table of registered KI files\n"
205
205
  "- 'create-adr' \u2014 workflow: document architectural decisions\n"
206
206
  "- 'expand-knowledge' \u2014 workflow: deep enrichment of KI files\n"
207
+ "- 'scaffold-knowledge' \u2014 workflow: bulk scaffold of uncovered modules\n"
207
208
  "- 'sync-knowledge' \u2014 workflow: sync KI after code changes\n"
208
- "- 'update-knowledge' \u2014 workflow: update KI files\n"
209
209
  "Call this before starting any documentation workflow task."
210
210
  ),
211
211
  "inputSchema": {
@@ -679,7 +679,11 @@ MCP_TOOLS = [
679
679
  ),
680
680
  "inputSchema": {
681
681
  "type": "object",
682
- "properties": {"rel_path": {"type": "string", "description": "Path relative to knowledge root/"}},
682
+ "properties": {
683
+ "rel_path": {"type": "string", "description": "Path relative to knowledge root/"},
684
+ "max_chars": {"type": "integer", "description": "Optional maximum number of characters to return"},
685
+ "offset": {"type": "integer", "default": 0, "description": "Optional character offset to start reading from"},
686
+ },
683
687
  "required": ["rel_path"],
684
688
  },
685
689
  "annotations": {
@@ -1198,35 +1202,76 @@ def tool_read_file(args: dict) -> Any:
1198
1202
  jail = get_jail_dir()
1199
1203
 
1200
1204
  target = None
1201
- # 1. Try resolving within jail (knowledge root)
1205
+ norm = ki_utils.normalize_path(rel_path, make_absolute=False)
1206
+ base_name = os.path.basename(norm)
1207
+
1208
+ # 1. Try resolving within jail (knowledge root from config, default .ki-base)
1202
1209
  if jail:
1210
+ # 1a. Direct path inside jail
1203
1211
  try:
1204
- cand = validate_path(rel_path)
1205
- if os.path.exists(cand):
1212
+ cand = validate_path(norm)
1213
+ if os.path.exists(cand) and os.path.isfile(cand):
1206
1214
  target = cand
1207
1215
  except Exception:
1208
1216
  target = None
1209
1217
 
1218
+ # 1b. Try inside jail/knowledge/ (standard structure: <jail>/knowledge/<file>)
1219
+ if not target:
1220
+ try:
1221
+ cand = validate_path(os.path.join("knowledge", norm))
1222
+ if os.path.exists(cand) and os.path.isfile(cand):
1223
+ target = cand
1224
+ except Exception:
1225
+ target = None
1226
+
1227
+ # 1c. If rel_path starts with the knowledge_root folder name itself (e.g. '.ki-base/...' or 'knowledge/...'),
1228
+ # strip the prefix and resolve inside jail
1229
+ if not target and project_root:
1230
+ rel_to_proj = os.path.relpath(jail, project_root).replace("\\", "/")
1231
+ if norm.startswith(rel_to_proj + "/"):
1232
+ sub_path = norm[len(rel_to_proj) + 1:]
1233
+ try:
1234
+ cand = validate_path(sub_path)
1235
+ if os.path.exists(cand) and os.path.isfile(cand):
1236
+ target = cand
1237
+ except Exception:
1238
+ pass
1239
+ if not target:
1240
+ try:
1241
+ cand = validate_path(os.path.join("knowledge", sub_path))
1242
+ if os.path.exists(cand) and os.path.isfile(cand):
1243
+ target = cand
1244
+ except Exception:
1245
+ pass
1246
+
1210
1247
  # 2. Try resolving ADR or path relative to project_root
1211
1248
  if not target and project_root:
1212
- norm = ki_utils.normalize_path(rel_path, make_absolute=False)
1213
1249
  candidates = ki_utils.get_decisions_dirs(project_root, jail)
1214
1250
  # Check by basename in decisions dirs
1215
1251
  for d in candidates:
1216
- cand_p = os.path.abspath(os.path.join(d, os.path.basename(norm)))
1217
- if os.path.exists(cand_p) and cand_p.endswith(".md"):
1252
+ cand_p = os.path.abspath(os.path.join(d, base_name))
1253
+ if os.path.exists(cand_p) and cand_p.endswith(".md") and os.path.isfile(cand_p):
1218
1254
  target = cand_p
1219
1255
  break
1220
1256
  if not target:
1221
1257
  # Check relative to project root, ensuring it is within decisions or jail
1222
1258
  cand_p = os.path.abspath(os.path.join(project_root, norm))
1223
- if os.path.exists(cand_p) and cand_p.endswith(".md"):
1259
+ if os.path.exists(cand_p) and cand_p.endswith(".md") and os.path.isfile(cand_p):
1224
1260
  valid_prefixes = candidates + ([jail] if jail else [])
1225
1261
  for vp in valid_prefixes:
1226
1262
  if os.path.normcase(cand_p).startswith(os.path.normcase(vp)):
1227
1263
  target = cand_p
1228
1264
  break
1229
1265
 
1266
+ # 3. Fallback: Search by basename in jail (knowledge root) and its subfolders
1267
+ if not target and jail and os.path.exists(jail):
1268
+ for root, _, files in os.walk(jail):
1269
+ if base_name in files:
1270
+ cand_p = os.path.abspath(os.path.join(root, base_name))
1271
+ if cand_p.endswith(".md") and os.path.isfile(cand_p):
1272
+ target = cand_p
1273
+ break
1274
+
1230
1275
  if not target or not os.path.exists(target):
1231
1276
  return f"File not found: '{rel_path}'."
1232
1277
 
@@ -1243,14 +1288,31 @@ def tool_read_file(args: dict) -> Any:
1243
1288
  return f"Section '{section}' not found in '{rel_path}'."
1244
1289
  content = sec_content
1245
1290
 
1291
+ offset = 0
1292
+ if args.get("offset") is not None:
1293
+ try:
1294
+ offset = max(0, int(args["offset"]))
1295
+ except (ValueError, TypeError):
1296
+ offset = 0
1297
+
1246
1298
  max_chars = args.get("max_chars")
1247
1299
  if max_chars is not None:
1248
1300
  try:
1249
1301
  mc = int(max_chars)
1250
- if mc > 0 and len(content) > mc:
1251
- content = content[:mc] + f"\n\n... [truncated to {mc} characters]"
1302
+ if mc > 0:
1303
+ total_len = len(content)
1304
+ chunk = content[offset : offset + mc]
1305
+ if offset + mc < total_len:
1306
+ content = chunk + f"\n\n... [truncated to {mc} characters, offset {offset} of {total_len}]"
1307
+ else:
1308
+ content = chunk
1309
+ elif offset > 0:
1310
+ content = content[offset:]
1252
1311
  except (ValueError, TypeError):
1253
- pass
1312
+ if offset > 0:
1313
+ content = content[offset:]
1314
+ elif offset > 0:
1315
+ content = content[offset:]
1254
1316
 
1255
1317
  return content
1256
1318
 
@@ -1302,8 +1364,10 @@ def handle_tool_call(name: str, args: dict) -> Any:
1302
1364
  return "\n".join(f"- {v['name']}: {k}" for k, v in reg["projects"].items())
1303
1365
  if name == "ki_status":
1304
1366
  match = ki_utils.find_project_by_cwd(args.get("path"))
1305
- return (f"Active: {match['name']} at {match['know_root']}" if match
1306
- else "No project active for current workspace.")
1367
+ if match:
1368
+ loc = get_jail_dir() or match.get("workspace", "")
1369
+ return f"Active: {match['name']} at {loc}"
1370
+ return "No project active for current workspace."
1307
1371
  if name == "ki_prune_registry":
1308
1372
  reg = ki_utils.load_registry()
1309
1373
  before = len(reg["projects"])
@@ -1401,7 +1465,28 @@ def handle_tool_call(name: str, args: dict) -> Any:
1401
1465
  # ─── File Ops ───
1402
1466
  if name == "read_know_file":
1403
1467
  with open(validate_path(args["rel_path"]), "r", encoding="utf-8") as f:
1404
- return f.read()
1468
+ content = f.read()
1469
+ offset = 0
1470
+ if args.get("offset") is not None:
1471
+ try:
1472
+ offset = max(0, int(args["offset"]))
1473
+ except (ValueError, TypeError):
1474
+ offset = 0
1475
+ max_chars = args.get("max_chars")
1476
+ if max_chars is not None:
1477
+ try:
1478
+ mc = int(max_chars)
1479
+ if mc > 0:
1480
+ total_len = len(content)
1481
+ chunk = content[offset : offset + mc]
1482
+ if offset + mc < total_len:
1483
+ return chunk + f"\n\n... [truncated to {mc} characters, offset {offset} of {total_len}]"
1484
+ return chunk
1485
+ except (ValueError, TypeError):
1486
+ pass
1487
+ if offset > 0:
1488
+ return content[offset:]
1489
+ return content
1405
1490
  if name == "write_know_file":
1406
1491
  p = validate_path(args["rel_path"], is_write=True)
1407
1492
  os.makedirs(os.path.dirname(p), exist_ok=True)
@@ -1454,9 +1539,13 @@ def handle_tool_call(name: str, args: dict) -> Any:
1454
1539
  if name == "ki_search":
1455
1540
  query = args.get("query", "").strip()
1456
1541
  scope = args.get("scope", "all")
1457
- limit = int(args.get("limit", 10))
1458
- res = ki_search.search_knowledge(query, project_root=get_project_root(), scope=scope, limit=limit)
1459
- return json.dumps(res, ensure_ascii=False, indent=2)
1542
+ limit = int(args.get("limit", 5))
1543
+ offset = int(args.get("offset", 0))
1544
+ fmt = args.get("format", "markdown")
1545
+ res = ki_search.search_knowledge(query, project_root=get_project_root(), scope=scope, limit=limit, offset=offset)
1546
+ if fmt == "json":
1547
+ return json.dumps(res, ensure_ascii=False, indent=2)
1548
+ return ki_search.format_search_markdown(res)
1460
1549
 
1461
1550
  if name == "ki_read":
1462
1551
  return tool_read_file(args)
@@ -87,8 +87,19 @@ KI_SEARCH_TOOL = {
87
87
  },
88
88
  "limit": {
89
89
  "type": "integer",
90
- "default": 10,
91
- "description": "Maximum number of results to return (default: 10).",
90
+ "default": 5,
91
+ "description": "Maximum number of results to return (default: 5).",
92
+ },
93
+ "offset": {
94
+ "type": "integer",
95
+ "default": 0,
96
+ "description": "Result offset for pagination (default: 0).",
97
+ },
98
+ "format": {
99
+ "type": "string",
100
+ "enum": ["markdown", "json"],
101
+ "default": "markdown",
102
+ "description": "Output format: 'markdown' (compact list, default) or 'json'.",
92
103
  },
93
104
  },
94
105
  "required": ["query"],
@@ -105,14 +116,14 @@ KI_READ_TOOL = {
105
116
  "name": "ki_read",
106
117
  "description": (
107
118
  "Read a Knowledge Item (KI) or ADR markdown file. "
108
- "Supports optional section filtering (by markdown header) and character limits to save tokens."
119
+ "Supports optional section filtering (by markdown header), character limits, and offset."
109
120
  ),
110
121
  "inputSchema": {
111
122
  "type": "object",
112
123
  "properties": {
113
124
  "rel_path": {
114
125
  "type": "string",
115
- "description": "Relative path to file from knowledge root or project root (e.g. '_OVERVIEW.ki.md' or 'decisions/0001_some.md').",
126
+ "description": "Path or filename of the Knowledge Item or ADR (e.g. 'KI_architecture.md', 'knowledge/KI_architecture.md', or '001_some.md').",
116
127
  },
117
128
  "section": {
118
129
  "type": "string",
@@ -122,6 +133,11 @@ KI_READ_TOOL = {
122
133
  "type": "integer",
123
134
  "description": "Optional maximum number of characters to return (truncates if exceeded).",
124
135
  },
136
+ "offset": {
137
+ "type": "integer",
138
+ "default": 0,
139
+ "description": "Optional character offset to start reading from (default: 0).",
140
+ },
125
141
  },
126
142
  "required": ["rel_path"],
127
143
  },
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ki-manager
3
- Version: 2.3.0.dev1
3
+ Version: 2.3.0.dev2
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,6 +1,6 @@
1
- ki_manager/__init__.py,sha256=UWIx-Cu-ZoJwkJh7Pr8urSzzO3IOpHCJWwRmXmCvpwo,104
1
+ ki_manager/__init__.py,sha256=PD9vsZa6-RlaTIp3gN1dcS0MSGm-gjZXaL_5ROLMGhQ,104
2
2
  ki_manager/cli.py,sha256=SOjh0AJ0YA3cpKEUIVx3t-tFEUq1ufjzweGQpfrxXXc,4800
3
- ki_manager/server.py,sha256=1zH8V3AKXp95aTKHmeJWnCKvZmI8ZfAObIAUfHggbmM,81884
3
+ ki_manager/server.py,sha256=OiWIN2EFVtR1pINeGn_SBO--Ut2y-_SWnNUuTg-r1Ss,85798
4
4
  ki_manager/scripts/add_ki_to_config.py,sha256=v7YhEVRpbD4U2x6FuGKuX3ZDp6CbXp8VnsoqWX9SJh8,2527
5
5
  ki_manager/scripts/analyze_module.py,sha256=m-wSQnKhOtjfQUjuYR3OI87AeZde0ZN8JfTbZp8MXTI,3966
6
6
  ki_manager/scripts/audit_coverage.py,sha256=NJkjTVS0ZwI4Uji6EpB1bM4zVDQp_aR925z_DJm5EHY,16600
@@ -10,19 +10,19 @@ ki_manager/scripts/generate_dir_index.py,sha256=ODIgA7QxhdBMLViRTk5CK4vv5XrZcqSo
10
10
  ki_manager/scripts/generate_ki_scaffolds.py,sha256=5Q9ygGScb8FZogexyXxum2Cu3hW19LjCJQQ-dGfSj5g,17271
11
11
  ki_manager/scripts/ki_dependency_analyzer.py,sha256=378pSZZX_nAAoh9QTFb_A5yqdKFfp8G7iqis0dJu6eg,9794
12
12
  ki_manager/scripts/ki_graph_visualize.py,sha256=3Xg6WrWliREeCUxKjmeivBYT56lFBbC3Jot7uD2e81g,9102
13
- ki_manager/scripts/ki_search.py,sha256=7WKBkySxmnGTzvHDrgEamnwevCJ5iMqP1qsfT6Vdv_c,10376
13
+ ki_manager/scripts/ki_search.py,sha256=qMtP8DTAEc-i8cinlxl1bmmioQU7j6QaSLSO4PXyBFU,11756
14
14
  ki_manager/scripts/ki_utils.py,sha256=S2WbXMxGVlx61nekjKRuA7vkX43xXBUlPeOPHb4sR3U,18644
15
15
  ki_manager/scripts/knowledge_engine.py,sha256=aV8FC2gWnF1Q8HtUDbL4P2uNZcFg01yPR51BzL6ZuMs,9742
16
16
  ki_manager/scripts/sync_agents_md.py,sha256=zpRzCnitS4o279d4Bqs04ybs8nR4a5TVZ-dKTInK3_w,5828
17
17
  ki_manager/tools/__init__.py,sha256=8pDeHnkj6ZAFI4grP9Exz-Jah99PVUGnuagiCbcVBQY,49
18
- ki_manager/tools/facade.py,sha256=7BaVKNbjoTitttK48yfBviAwb6BIurzIzcsQNYU5VRY,13095
18
+ ki_manager/tools/facade.py,sha256=PIcpGbUhnAcA73NJvp9gN3YEkIr4N3gKRPPUakyqxFE,13734
19
19
  ki_manager/tools/scaffold.py,sha256=UTHFGmvYxO8xGULZXZZUW0CEw7cw0rNNJw0Qf2ke0Ew,10981
20
20
  ki_manager/workflows/create-adr.md,sha256=Z67sPc4cjVwwTFbURWLBdax4VrT5C3VmoacQE2HAZJk,4240
21
21
  ki_manager/workflows/expand-knowledge.md,sha256=aauyUAfL4bYMqaZPuxVjVYPi5mSbGs0CV6GctBgNAKI,6099
22
22
  ki_manager/workflows/scaffold-knowledge.md,sha256=a67Nj2u9Pv7m0Q249f1AE-sHIlXzeOdupAYysxzBHxE,8052
23
23
  ki_manager/workflows/sync-knowledge.md,sha256=1RGg1MqlDLSiL03yrNyrYhFLw65xdAgdVAZFyRGLOKU,3906
24
- ki_manager-2.3.0.dev1.dist-info/METADATA,sha256=-Rlv-wUhqndITqJTsddfu4cWA02S288yegjMvEapayk,11111
25
- ki_manager-2.3.0.dev1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
26
- ki_manager-2.3.0.dev1.dist-info/entry_points.txt,sha256=V-NMpOuZiRNc4hDwZ0AcWnEltRkc_T4WD_sUxOQf7TE,94
27
- ki_manager-2.3.0.dev1.dist-info/licenses/LICENSE,sha256=KBvPtUXGgjjiHBWYvaNazfiZ_U-VYaqisz_yVCUfOBs,1072
28
- ki_manager-2.3.0.dev1.dist-info/RECORD,,
24
+ ki_manager-2.3.0.dev2.dist-info/METADATA,sha256=eD22pNIX587Iq5hGl-CH_aJPcNsZxLV4cT3Bl1wSzvE,11111
25
+ ki_manager-2.3.0.dev2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
26
+ ki_manager-2.3.0.dev2.dist-info/entry_points.txt,sha256=V-NMpOuZiRNc4hDwZ0AcWnEltRkc_T4WD_sUxOQf7TE,94
27
+ ki_manager-2.3.0.dev2.dist-info/licenses/LICENSE,sha256=KBvPtUXGgjjiHBWYvaNazfiZ_U-VYaqisz_yVCUfOBs,1072
28
+ ki_manager-2.3.0.dev2.dist-info/RECORD,,