Skip to content

Commit 542ad7a

Browse files
committed
Add slash commands (/status, /list, /lint, /add) to chat REPL
Extract core logic from CLI commands into reusable functions (print_status, print_list, run_lint) so the chat REPL can call them directly instead of duplicating code. /add uses asyncio.to_thread since _add_single_file internally calls asyncio.run which cannot be nested.
1 parent 0b19f9c commit 542ad7a

2 files changed

Lines changed: 92 additions & 20 deletions

File tree

openkb/agent/chat.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@
4646
" /exit Exit (Ctrl-D also works)\n"
4747
" /clear Start a fresh session (current one is kept on disk)\n"
4848
" /save [name] Export transcript to wiki/explorations/\n"
49+
" /status Show knowledge base status\n"
50+
" /list List all documents in the knowledge base\n"
51+
" /lint Lint the knowledge base\n"
52+
" /add <path> Add a document or directory to the knowledge base\n"
4953
" /help Show this"
5054
)
5155

@@ -271,6 +275,37 @@ def _save_transcript(kb_dir: Path, session: ChatSession, name: str | None) -> Pa
271275
return path
272276

273277

278+
async def _run_add(arg: str, kb_dir: Path, style: Style) -> None:
279+
"""Add a document or directory to the knowledge base from the chat REPL."""
280+
import asyncio
281+
from openkb.cli import _add_single_file, SUPPORTED_EXTENSIONS
282+
283+
target = Path(arg).expanduser()
284+
if not target.is_absolute():
285+
target = Path.cwd() / target
286+
target = target.resolve()
287+
288+
if not target.exists():
289+
_fmt(style, ("class:error", f"Path does not exist: {arg}\n"))
290+
return
291+
292+
if target.is_dir():
293+
files = [
294+
f for f in sorted(target.rglob("*"))
295+
if f.is_file() and f.suffix.lower() in SUPPORTED_EXTENSIONS
296+
]
297+
if not files:
298+
_fmt(style, ("class:error", f"No supported files found in {arg}.\n"))
299+
return
300+
for f in files:
301+
await asyncio.to_thread(_add_single_file, f, kb_dir)
302+
else:
303+
if target.suffix.lower() not in SUPPORTED_EXTENSIONS:
304+
_fmt(style, ("class:error", f"Unsupported file type: {target.suffix}\n"))
305+
return
306+
await asyncio.to_thread(_add_single_file, target, kb_dir)
307+
308+
274309
async def _handle_slash(
275310
cmd: str,
276311
kb_dir: Path,
@@ -307,6 +342,28 @@ async def _handle_slash(
307342
_fmt(style, ("class:slash.ok", f"Saved to {path}\n"))
308343
return None
309344

345+
if head == "/status":
346+
from openkb.cli import print_status
347+
print_status(kb_dir)
348+
return None
349+
350+
if head == "/list":
351+
from openkb.cli import print_list
352+
print_list(kb_dir)
353+
return None
354+
355+
if head == "/lint":
356+
from openkb.cli import run_lint
357+
await run_lint(kb_dir)
358+
return None
359+
360+
if head == "/add":
361+
if not arg:
362+
_fmt(style, ("class:error", "Usage: /add <path>\n"))
363+
return None
364+
await _run_add(arg, kb_dir, style)
365+
return None
366+
310367
_fmt(
311368
style,
312369
("class:error", f"Unknown command: {head}. Try /help.\n"),

openkb/cli.py

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -525,18 +525,12 @@ def on_new_files(paths):
525525
watch_directory(raw_dir, on_new_files)
526526

527527

528-
@cli.command()
529-
@click.option("--fix", is_flag=True, default=False, help="Automatically fix lint issues (not yet implemented).")
530-
@click.pass_context
531-
def lint(ctx, fix):
532-
"""Lint the knowledge base for structural and semantic inconsistencies."""
533-
if fix:
534-
click.echo("Warning: --fix is not yet implemented. Running lint in report-only mode.")
535-
kb_dir = _find_kb_dir(ctx.obj.get("kb_dir_override"))
536-
if kb_dir is None:
537-
click.echo("No knowledge base found. Run `openkb init` first.")
538-
return
528+
async def run_lint(kb_dir: Path) -> Path:
529+
"""Run structural + knowledge lint, write report, return report path.
539530
531+
Async because knowledge lint uses an LLM agent. Usable from CLI
532+
(via ``asyncio.run``) and directly from the chat REPL.
533+
"""
540534
from openkb.lint import run_structural_lint
541535
from openkb.agent.linter import run_knowledge_lint
542536

@@ -556,15 +550,13 @@ def lint(ctx, fix):
556550
_setup_llm_key(kb_dir)
557551
model: str = config.get("model", DEFAULT_CONFIG["model"])
558552

559-
# Structural lint
560553
click.echo("Running structural lint...")
561554
structural_report = run_structural_lint(kb_dir)
562555
click.echo(structural_report)
563556

564-
# Knowledge lint (semantic)
565557
click.echo("Running knowledge lint...")
566558
try:
567-
knowledge_report = asyncio.run(run_knowledge_lint(kb_dir, model))
559+
knowledge_report = await run_knowledge_lint(kb_dir, model)
568560
except Exception as exc:
569561
knowledge_report = f"Knowledge lint failed: {exc}"
570562
click.echo(knowledge_report)
@@ -579,17 +571,25 @@ def lint(ctx, fix):
579571
report_path.write_text(report_content, encoding="utf-8")
580572
append_log(kb_dir / "wiki", "lint", f"report → {report_path.name}")
581573
click.echo(f"\nReport written to {report_path}")
574+
return report_path
582575

583576

584-
@cli.command(name="list")
577+
@cli.command()
578+
@click.option("--fix", is_flag=True, default=False, help="Automatically fix lint issues (not yet implemented).")
585579
@click.pass_context
586-
def list_cmd(ctx):
587-
"""List all documents in the knowledge base."""
580+
def lint(ctx, fix):
581+
"""Lint the knowledge base for structural and semantic inconsistencies."""
582+
if fix:
583+
click.echo("Warning: --fix is not yet implemented. Running lint in report-only mode.")
588584
kb_dir = _find_kb_dir(ctx.obj.get("kb_dir_override"))
589585
if kb_dir is None:
590586
click.echo("No knowledge base found. Run `openkb init` first.")
591587
return
588+
asyncio.run(run_lint(kb_dir))
589+
592590

591+
def print_list(kb_dir: Path) -> None:
592+
"""Print all documents in the knowledge base. Usable from CLI and chat REPL."""
593593
openkb_dir = kb_dir / ".openkb"
594594
hashes_file = openkb_dir / "hashes.json"
595595
if not hashes_file.exists():
@@ -642,15 +642,19 @@ def list_cmd(ctx):
642642
click.echo(f" - {r}")
643643

644644

645-
@cli.command()
645+
@cli.command(name="list")
646646
@click.pass_context
647-
def status(ctx):
648-
"""Show the current status of the knowledge base."""
647+
def list_cmd(ctx):
648+
"""List all documents in the knowledge base."""
649649
kb_dir = _find_kb_dir(ctx.obj.get("kb_dir_override"))
650650
if kb_dir is None:
651651
click.echo("No knowledge base found. Run `openkb init` first.")
652652
return
653+
print_list(kb_dir)
653654

655+
656+
def print_status(kb_dir: Path) -> None:
657+
"""Print knowledge base status. Usable from CLI and chat REPL."""
654658
wiki_dir = kb_dir / "wiki"
655659
subdirs = ["sources", "summaries", "concepts", "reports"]
656660

@@ -698,3 +702,14 @@ def status(ctx):
698702
import datetime
699703
mtime = datetime.datetime.fromtimestamp(newest_report.stat().st_mtime)
700704
click.echo(f" Last lint: {mtime.strftime('%Y-%m-%d %H:%M:%S')}")
705+
706+
707+
@cli.command()
708+
@click.pass_context
709+
def status(ctx):
710+
"""Show the current status of the knowledge base."""
711+
kb_dir = _find_kb_dir(ctx.obj.get("kb_dir_override"))
712+
if kb_dir is None:
713+
click.echo("No knowledge base found. Run `openkb init` first.")
714+
return
715+
print_status(kb_dir)

0 commit comments

Comments
 (0)