kc-cli 0.4.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.
- kc/__init__.py +5 -0
- kc/__main__.py +11 -0
- kc/artifacts/__init__.py +1 -0
- kc/artifacts/diff.py +76 -0
- kc/artifacts/frontmatter.py +26 -0
- kc/artifacts/markdown.py +116 -0
- kc/atomic_write.py +33 -0
- kc/cli.py +284 -0
- kc/commands/__init__.py +1 -0
- kc/commands/artifact.py +1190 -0
- kc/commands/citation.py +231 -0
- kc/commands/common.py +346 -0
- kc/commands/conformance.py +293 -0
- kc/commands/context.py +190 -0
- kc/commands/doctor.py +81 -0
- kc/commands/eval.py +133 -0
- kc/commands/export.py +97 -0
- kc/commands/guide.py +571 -0
- kc/commands/index.py +54 -0
- kc/commands/init.py +207 -0
- kc/commands/lint.py +238 -0
- kc/commands/source.py +464 -0
- kc/commands/status.py +52 -0
- kc/commands/task.py +260 -0
- kc/config.py +127 -0
- kc/embedding_models/potion-base-8M/README.md +97 -0
- kc/embedding_models/potion-base-8M/config.json +13 -0
- kc/embedding_models/potion-base-8M/model.safetensors +0 -0
- kc/embedding_models/potion-base-8M/modules.json +14 -0
- kc/embedding_models/potion-base-8M/tokenizer.json +1 -0
- kc/errors.py +141 -0
- kc/fingerprints.py +35 -0
- kc/ids.py +23 -0
- kc/locks.py +65 -0
- kc/models/__init__.py +17 -0
- kc/models/artifact.py +34 -0
- kc/models/citation.py +60 -0
- kc/models/context.py +23 -0
- kc/models/eval.py +21 -0
- kc/models/plan.py +37 -0
- kc/models/source.py +37 -0
- kc/models/source_range.py +29 -0
- kc/models/source_revision.py +19 -0
- kc/models/task.py +35 -0
- kc/output.py +838 -0
- kc/paths.py +126 -0
- kc/provenance/__init__.py +1 -0
- kc/provenance/citations.py +296 -0
- kc/search/__init__.py +1 -0
- kc/search/extract.py +268 -0
- kc/search/fts.py +284 -0
- kc/search/semantic.py +346 -0
- kc/store/__init__.py +1 -0
- kc/store/jsonl.py +55 -0
- kc/store/sqlite.py +444 -0
- kc/store/transaction.py +67 -0
- kc/templates/agents/skills/kc/SKILL.md +282 -0
- kc/templates/agents/skills/kc/agents/openai.yaml +5 -0
- kc/templates/agents/skills/kc/scripts/resolve_query_citations.py +134 -0
- kc/workspace.py +98 -0
- kc_cli-0.4.0.dist-info/METADATA +522 -0
- kc_cli-0.4.0.dist-info/RECORD +65 -0
- kc_cli-0.4.0.dist-info/WHEEL +4 -0
- kc_cli-0.4.0.dist-info/entry_points.txt +2 -0
- kc_cli-0.4.0.dist-info/licenses/LICENSE +21 -0
kc/__init__.py
ADDED
kc/__main__.py
ADDED
kc/artifacts/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Artifact parsing, validation, and diff helpers."""
|
kc/artifacts/diff.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Structured artifact diff planning."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import difflib
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from kc.fingerprints import raw_fingerprint
|
|
10
|
+
from kc.ids import new_id
|
|
11
|
+
from kc.models.plan import PlanCondition, PlanOperation, PlanRecord
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def build_artifact_plan(
|
|
15
|
+
path: Path,
|
|
16
|
+
*,
|
|
17
|
+
registered_fingerprint: str | None,
|
|
18
|
+
baseline_path: Path | None = None,
|
|
19
|
+
mode: str = "dry_run",
|
|
20
|
+
idempotency_key: str | None = None,
|
|
21
|
+
) -> tuple[PlanRecord, str, dict[str, str | None]]:
|
|
22
|
+
after = raw_fingerprint(path) if path.exists() else None
|
|
23
|
+
before = registered_fingerprint
|
|
24
|
+
baseline: dict[str, str | None] = {"kind": "unavailable", "path": None, "fingerprint": before}
|
|
25
|
+
old_lines: list[str] = []
|
|
26
|
+
if baseline_path is not None and baseline_path.exists():
|
|
27
|
+
old_lines = baseline_path.read_text(encoding="utf-8-sig").splitlines(keepends=True)
|
|
28
|
+
baseline = {
|
|
29
|
+
"kind": "last_applied_snapshot",
|
|
30
|
+
"path": baseline_path.as_posix(),
|
|
31
|
+
"fingerprint": raw_fingerprint(baseline_path),
|
|
32
|
+
}
|
|
33
|
+
new_lines = (
|
|
34
|
+
path.read_text(encoding="utf-8-sig").splitlines(keepends=True) if path.exists() else []
|
|
35
|
+
)
|
|
36
|
+
if before is None:
|
|
37
|
+
risk_flags = ["new_artifact"]
|
|
38
|
+
old_label = "/dev/null"
|
|
39
|
+
else:
|
|
40
|
+
risk_flags = ["updates_existing_artifact"] if before != after else []
|
|
41
|
+
old_label = baseline_path.as_posix() if baseline_path is not None and baseline_path.exists() else f"{path.as_posix()}@registry"
|
|
42
|
+
diff_text = "".join(
|
|
43
|
+
difflib.unified_diff(
|
|
44
|
+
old_lines,
|
|
45
|
+
new_lines,
|
|
46
|
+
fromfile=old_label,
|
|
47
|
+
tofile=path.as_posix(),
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
risk = "medium" if risk_flags else "low"
|
|
51
|
+
plan = PlanRecord(
|
|
52
|
+
plan_id=new_id("plan"),
|
|
53
|
+
created_at=datetime.now(UTC).isoformat(),
|
|
54
|
+
command="artifact.apply",
|
|
55
|
+
mode=mode, # type: ignore[arg-type]
|
|
56
|
+
idempotency_key=idempotency_key,
|
|
57
|
+
operations=[
|
|
58
|
+
PlanOperation(
|
|
59
|
+
op_id="op_01",
|
|
60
|
+
kind="register_artifact",
|
|
61
|
+
path=path.as_posix(),
|
|
62
|
+
before_fingerprint=before,
|
|
63
|
+
after_fingerprint=after,
|
|
64
|
+
risk=risk, # type: ignore[arg-type]
|
|
65
|
+
requires_yes=True,
|
|
66
|
+
)
|
|
67
|
+
],
|
|
68
|
+
preconditions=[
|
|
69
|
+
PlanCondition(kind="file_exists", path=path.as_posix(), expected="true"),
|
|
70
|
+
],
|
|
71
|
+
postconditions=[
|
|
72
|
+
PlanCondition(kind="artifact_validates", path=path.as_posix()),
|
|
73
|
+
],
|
|
74
|
+
risk_flags=risk_flags,
|
|
75
|
+
)
|
|
76
|
+
return plan, diff_text, baseline
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Markdown frontmatter parsing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parse_frontmatter(text: str) -> tuple[dict[str, Any], str]:
|
|
11
|
+
if not text.startswith("---\n") and not text.startswith("---\r\n"):
|
|
12
|
+
return {}, text
|
|
13
|
+
normalized = text.replace("\r\n", "\n")
|
|
14
|
+
parts = normalized.split("---\n", 2)
|
|
15
|
+
if len(parts) < 3:
|
|
16
|
+
return {}, text
|
|
17
|
+
raw = parts[1]
|
|
18
|
+
body = parts[2]
|
|
19
|
+
data = yaml.safe_load(raw) or {}
|
|
20
|
+
if not isinstance(data, dict):
|
|
21
|
+
data = {}
|
|
22
|
+
return data, body
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def dump_frontmatter(data: dict[str, Any], body: str) -> str:
|
|
26
|
+
return "---\n" + yaml.safe_dump(data, sort_keys=False) + "---\n" + body.lstrip("\n")
|
kc/artifacts/markdown.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Markdown artifact helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from kc.artifacts.frontmatter import parse_frontmatter
|
|
9
|
+
from kc.provenance.citations import has_citation_or_marker
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def read_markdown_artifact(path: Path) -> tuple[dict[str, Any], str, str]:
|
|
13
|
+
text = path.read_text(encoding="utf-8-sig")
|
|
14
|
+
frontmatter, body = parse_frontmatter(text)
|
|
15
|
+
return frontmatter, body, text
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def markdown_body_line_offset(text: str) -> int:
|
|
19
|
+
normalized = text.replace("\r\n", "\n")
|
|
20
|
+
if not normalized.startswith("---\n"):
|
|
21
|
+
return 0
|
|
22
|
+
parts = normalized.split("---\n", 2)
|
|
23
|
+
if len(parts) < 3:
|
|
24
|
+
return 0
|
|
25
|
+
return ("---\n" + parts[1] + "---\n").count("\n")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def required_section_names(body: str) -> set[str]:
|
|
29
|
+
headings = set()
|
|
30
|
+
for line in body.splitlines():
|
|
31
|
+
if line.startswith("## "):
|
|
32
|
+
headings.add(line[3:].strip().lower())
|
|
33
|
+
return headings
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def citation_coverage_issues(
|
|
37
|
+
body: str,
|
|
38
|
+
*,
|
|
39
|
+
status: str,
|
|
40
|
+
requires_citations: bool,
|
|
41
|
+
allow_uncited: bool,
|
|
42
|
+
line_offset: int = 0,
|
|
43
|
+
) -> list[dict[str, Any]]:
|
|
44
|
+
issues: list[dict[str, Any]] = []
|
|
45
|
+
if not requires_citations:
|
|
46
|
+
return issues
|
|
47
|
+
|
|
48
|
+
in_code = False
|
|
49
|
+
paragraph: list[tuple[int, str]] = []
|
|
50
|
+
|
|
51
|
+
def flush() -> None:
|
|
52
|
+
nonlocal paragraph
|
|
53
|
+
if not paragraph:
|
|
54
|
+
return
|
|
55
|
+
text = " ".join(line for _line_no, line in paragraph).strip()
|
|
56
|
+
first_line = paragraph[0][0]
|
|
57
|
+
paragraph = []
|
|
58
|
+
if not text:
|
|
59
|
+
return
|
|
60
|
+
if text.startswith("#"):
|
|
61
|
+
return
|
|
62
|
+
if text.startswith("|") and text.endswith("|"):
|
|
63
|
+
return
|
|
64
|
+
if has_citation_or_marker(text):
|
|
65
|
+
if "[kc:uncited]" in text and not allow_uncited:
|
|
66
|
+
issues.append(
|
|
67
|
+
{
|
|
68
|
+
"code": "KC_VALIDATION_MISSING_CITATION",
|
|
69
|
+
"message": "[kc:uncited] is not allowed without --allow-uncited.",
|
|
70
|
+
"line": first_line,
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
if "[kc:todo]" in text and status != "draft":
|
|
74
|
+
issues.append(
|
|
75
|
+
{
|
|
76
|
+
"code": "KC_VALIDATION_TODO_IN_ACTIVE_ARTIFACT",
|
|
77
|
+
"message": "[kc:todo] is allowed only for draft artifacts.",
|
|
78
|
+
"line": first_line,
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
return
|
|
82
|
+
issues.append(
|
|
83
|
+
{
|
|
84
|
+
"code": "KC_VALIDATION_MISSING_CITATION",
|
|
85
|
+
"message": "Paragraph requires at least one citation token or explicit kc marker.",
|
|
86
|
+
"line": first_line,
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
for line_no, raw_line in enumerate(body.splitlines(), start=1 + line_offset):
|
|
91
|
+
line = raw_line.strip()
|
|
92
|
+
if line.startswith("```"):
|
|
93
|
+
flush()
|
|
94
|
+
in_code = not in_code
|
|
95
|
+
continue
|
|
96
|
+
if in_code:
|
|
97
|
+
continue
|
|
98
|
+
if not line:
|
|
99
|
+
flush()
|
|
100
|
+
continue
|
|
101
|
+
if line.startswith("#"):
|
|
102
|
+
flush()
|
|
103
|
+
continue
|
|
104
|
+
paragraph.append((line_no, line))
|
|
105
|
+
flush()
|
|
106
|
+
return issues
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def markdown_title(frontmatter: dict[str, Any], body: str, fallback: str) -> str:
|
|
110
|
+
title = frontmatter.get("title")
|
|
111
|
+
if isinstance(title, str) and title.strip():
|
|
112
|
+
return title.strip()
|
|
113
|
+
for line in body.splitlines():
|
|
114
|
+
if line.startswith("# "):
|
|
115
|
+
return line[2:].strip()
|
|
116
|
+
return fallback
|
kc/atomic_write.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Atomic file writing helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def atomic_write_bytes(target: Path, data: bytes) -> None:
|
|
12
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
13
|
+
fd, tmp_name = tempfile.mkstemp(dir=target.parent, prefix=".kc_tmp_", suffix=target.suffix)
|
|
14
|
+
tmp_path = Path(tmp_name)
|
|
15
|
+
try:
|
|
16
|
+
with os.fdopen(fd, "wb") as f:
|
|
17
|
+
f.write(data)
|
|
18
|
+
f.flush()
|
|
19
|
+
os.fsync(f.fileno())
|
|
20
|
+
os.replace(tmp_path, target)
|
|
21
|
+
except Exception:
|
|
22
|
+
if tmp_path.exists():
|
|
23
|
+
tmp_path.unlink()
|
|
24
|
+
raise
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def atomic_write_text(target: Path, text: str) -> None:
|
|
28
|
+
atomic_write_bytes(target, text.encode("utf-8"))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def copy_snapshot(src: Path, dest: Path) -> None:
|
|
32
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
shutil.copy2(src, dest)
|
kc/cli.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Typer CLI application for kc."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Annotated, Any
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
import typer
|
|
10
|
+
from typer.core import TyperGroup
|
|
11
|
+
|
|
12
|
+
from kc.errors import KcError
|
|
13
|
+
from kc.output import emit_error, init_request, is_interactive, is_llm_mode, state
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _value_after(args: list[str], option: str) -> str | None:
|
|
17
|
+
for idx, item in enumerate(args):
|
|
18
|
+
if item == option and idx + 1 < len(args):
|
|
19
|
+
return args[idx + 1]
|
|
20
|
+
if item.startswith(f"{option}="):
|
|
21
|
+
return item.split("=", 1)[1]
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _initialize_error_state(args: list[str]) -> None:
|
|
26
|
+
if not state.request_id:
|
|
27
|
+
init_request(_value_after(args, "--request-id"))
|
|
28
|
+
requested_format = _value_after(args, "--format") or _value_after(args, "-f")
|
|
29
|
+
state.format = requested_format if requested_format in {"json", "table", "markdown"} else "json"
|
|
30
|
+
if is_llm_mode():
|
|
31
|
+
state.format = "json"
|
|
32
|
+
state.root_override = _value_after(args, "--root")
|
|
33
|
+
state.data_dir = _value_after(args, "--data-dir")
|
|
34
|
+
state.state_dir = _value_after(args, "--state-dir")
|
|
35
|
+
state.quiet = True
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _command_id_from_args(args: list[str]) -> str:
|
|
39
|
+
value_opts = {"--format", "-f", "--root", "--data-dir", "--state-dir", "--request-id"}
|
|
40
|
+
top_level = {
|
|
41
|
+
"guide",
|
|
42
|
+
"conformance",
|
|
43
|
+
"init",
|
|
44
|
+
"status",
|
|
45
|
+
"lint",
|
|
46
|
+
"export",
|
|
47
|
+
"source",
|
|
48
|
+
"index",
|
|
49
|
+
"context",
|
|
50
|
+
"artifact",
|
|
51
|
+
"citation",
|
|
52
|
+
"task",
|
|
53
|
+
"eval",
|
|
54
|
+
"doctor",
|
|
55
|
+
}
|
|
56
|
+
tokens: list[str] = []
|
|
57
|
+
index = 0
|
|
58
|
+
while index < len(args):
|
|
59
|
+
item = args[index]
|
|
60
|
+
if item in value_opts:
|
|
61
|
+
index += 2
|
|
62
|
+
continue
|
|
63
|
+
if any(item.startswith(f"{opt}=") for opt in value_opts):
|
|
64
|
+
index += 1
|
|
65
|
+
continue
|
|
66
|
+
if item.startswith("-"):
|
|
67
|
+
index += 1
|
|
68
|
+
continue
|
|
69
|
+
tokens.append(item)
|
|
70
|
+
if len(tokens) == 2:
|
|
71
|
+
break
|
|
72
|
+
index += 1
|
|
73
|
+
if not tokens or tokens[0] not in top_level:
|
|
74
|
+
return "kc"
|
|
75
|
+
if tokens[0] in {"source", "index", "context", "artifact", "citation", "task", "eval"} and len(tokens) > 1:
|
|
76
|
+
return f"{tokens[0]}.{tokens[1]}"
|
|
77
|
+
if tokens[0] == "doctor" and len(tokens) > 1:
|
|
78
|
+
return f"doctor.{tokens[1]}"
|
|
79
|
+
return tokens[0]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class FlexibleGroup(TyperGroup):
|
|
83
|
+
"""Allow root global options before or after the first subcommand."""
|
|
84
|
+
|
|
85
|
+
_VALUE_OPTS = frozenset(("--format", "-f", "--root", "--data-dir", "--state-dir", "--request-id"))
|
|
86
|
+
_FLAG_OPTS = frozenset(("--quiet", "-q", "--no-input", "--version", "-V"))
|
|
87
|
+
_VALUE_PREFIXES = ("--format=", "--root=", "--data-dir=", "--state-dir=", "--request-id=")
|
|
88
|
+
|
|
89
|
+
def main(
|
|
90
|
+
self,
|
|
91
|
+
args: list[str] | None = None,
|
|
92
|
+
prog_name: str | None = None,
|
|
93
|
+
complete_var: str | None = None,
|
|
94
|
+
standalone_mode: bool = True,
|
|
95
|
+
**extra: Any,
|
|
96
|
+
) -> object:
|
|
97
|
+
raw_args = list(sys.argv[1:] if args is None else args)
|
|
98
|
+
try:
|
|
99
|
+
result = super().main(
|
|
100
|
+
args=raw_args,
|
|
101
|
+
prog_name=prog_name,
|
|
102
|
+
complete_var=complete_var,
|
|
103
|
+
standalone_mode=False,
|
|
104
|
+
**extra,
|
|
105
|
+
)
|
|
106
|
+
if standalone_mode:
|
|
107
|
+
raise SystemExit(result if isinstance(result, int) else 0)
|
|
108
|
+
return result
|
|
109
|
+
except click.UsageError as exc:
|
|
110
|
+
if not standalone_mode:
|
|
111
|
+
raise
|
|
112
|
+
_initialize_error_state(raw_args)
|
|
113
|
+
emit_error(
|
|
114
|
+
_command_id_from_args(raw_args),
|
|
115
|
+
KcError(
|
|
116
|
+
code="KC_USAGE_ERROR",
|
|
117
|
+
message=exc.format_message(),
|
|
118
|
+
details={"usage": exc.format_message()},
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
|
|
123
|
+
cmd_idx: int | None = None
|
|
124
|
+
i = 0
|
|
125
|
+
while i < len(args):
|
|
126
|
+
item = args[i]
|
|
127
|
+
if not item.startswith("-"):
|
|
128
|
+
cmd_idx = i
|
|
129
|
+
break
|
|
130
|
+
if item in self._VALUE_OPTS:
|
|
131
|
+
i += 2
|
|
132
|
+
continue
|
|
133
|
+
if item.startswith(self._VALUE_PREFIXES):
|
|
134
|
+
i += 1
|
|
135
|
+
continue
|
|
136
|
+
i += 1
|
|
137
|
+
if cmd_idx is None:
|
|
138
|
+
return super().parse_args(ctx, args)
|
|
139
|
+
before = list(args[:cmd_idx])
|
|
140
|
+
cmd_and_after = list(args[cmd_idx:])
|
|
141
|
+
cmd_name = cmd_and_after[0]
|
|
142
|
+
sub_cmd = self.commands.get(cmd_name) if self.commands else None
|
|
143
|
+
sub_opts: set[str] = set()
|
|
144
|
+
if sub_cmd:
|
|
145
|
+
for param in sub_cmd.params:
|
|
146
|
+
sub_opts.update(param.opts)
|
|
147
|
+
sub_opts.update(getattr(param, "secondary_opts", []))
|
|
148
|
+
moved: list[str] = []
|
|
149
|
+
kept: list[str] = [cmd_name]
|
|
150
|
+
i = 1
|
|
151
|
+
end_of_opts = False
|
|
152
|
+
while i < len(cmd_and_after):
|
|
153
|
+
item = cmd_and_after[i]
|
|
154
|
+
if item == "--":
|
|
155
|
+
end_of_opts = True
|
|
156
|
+
kept.append(item)
|
|
157
|
+
i += 1
|
|
158
|
+
continue
|
|
159
|
+
if not end_of_opts and item in self._VALUE_OPTS and item not in sub_opts:
|
|
160
|
+
moved.append(item)
|
|
161
|
+
if i + 1 < len(cmd_and_after):
|
|
162
|
+
i += 1
|
|
163
|
+
moved.append(cmd_and_after[i])
|
|
164
|
+
i += 1
|
|
165
|
+
continue
|
|
166
|
+
if not end_of_opts and item in self._FLAG_OPTS and item not in sub_opts:
|
|
167
|
+
moved.append(item)
|
|
168
|
+
i += 1
|
|
169
|
+
continue
|
|
170
|
+
if (
|
|
171
|
+
not end_of_opts
|
|
172
|
+
and item.startswith(self._VALUE_PREFIXES)
|
|
173
|
+
and not any(item.startswith(f"{opt}=") for opt in sub_opts)
|
|
174
|
+
):
|
|
175
|
+
moved.append(item)
|
|
176
|
+
i += 1
|
|
177
|
+
continue
|
|
178
|
+
kept.append(item)
|
|
179
|
+
i += 1
|
|
180
|
+
return super().parse_args(ctx, before + moved + kept)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
app = typer.Typer(
|
|
184
|
+
name="kc",
|
|
185
|
+
cls=FlexibleGroup,
|
|
186
|
+
help=(
|
|
187
|
+
"kc — deterministic knowledge compiler harness.\n\n"
|
|
188
|
+
"Agents provide the intelligence. kc provides source registration, search, "
|
|
189
|
+
"context preparation, citation validation, safe apply, and task state."
|
|
190
|
+
),
|
|
191
|
+
no_args_is_help=True,
|
|
192
|
+
pretty_exceptions_enable=False,
|
|
193
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _version_callback(value: bool) -> None:
|
|
198
|
+
if value:
|
|
199
|
+
from kc import __version__
|
|
200
|
+
|
|
201
|
+
typer.echo(f"kc {__version__}")
|
|
202
|
+
raise typer.Exit()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@app.callback()
|
|
206
|
+
def main(
|
|
207
|
+
format: Annotated[
|
|
208
|
+
str,
|
|
209
|
+
typer.Option("--format", "-f", help="Output format: json, table, or markdown."),
|
|
210
|
+
] = "json",
|
|
211
|
+
root: Annotated[
|
|
212
|
+
str | None, typer.Option("--root", help="Workspace root override.")
|
|
213
|
+
] = None,
|
|
214
|
+
data_dir: Annotated[
|
|
215
|
+
str | None, typer.Option("--data-dir", help="Knowledge data directory override.")
|
|
216
|
+
] = None,
|
|
217
|
+
state_dir: Annotated[str | None, typer.Option("--state-dir", help="kc state directory override.")] = None,
|
|
218
|
+
quiet: Annotated[
|
|
219
|
+
bool, typer.Option("--quiet", "-q", help="Suppress stderr diagnostics.")
|
|
220
|
+
] = False,
|
|
221
|
+
request_id: Annotated[
|
|
222
|
+
str | None, typer.Option("--request-id", help="Trace request ID.")
|
|
223
|
+
] = None,
|
|
224
|
+
no_input: Annotated[
|
|
225
|
+
bool, typer.Option("--no-input", help="Fail instead of prompting.")
|
|
226
|
+
] = False,
|
|
227
|
+
version: Annotated[
|
|
228
|
+
bool,
|
|
229
|
+
typer.Option(
|
|
230
|
+
"--version",
|
|
231
|
+
"-V",
|
|
232
|
+
help="Show version and exit.",
|
|
233
|
+
callback=_version_callback,
|
|
234
|
+
is_eager=True,
|
|
235
|
+
),
|
|
236
|
+
] = False,
|
|
237
|
+
) -> None:
|
|
238
|
+
del version
|
|
239
|
+
init_request(request_id)
|
|
240
|
+
state.root_override = root
|
|
241
|
+
state.data_dir = data_dir
|
|
242
|
+
state.state_dir = state_dir
|
|
243
|
+
state.workspace_root = None
|
|
244
|
+
state.workspace_resolution_source = None
|
|
245
|
+
state.no_input = no_input or is_llm_mode()
|
|
246
|
+
if format not in {"json", "table", "markdown"}:
|
|
247
|
+
from kc.errors import KcError
|
|
248
|
+
from kc.output import emit_error
|
|
249
|
+
|
|
250
|
+
state.format = "json"
|
|
251
|
+
emit_error(
|
|
252
|
+
"kc",
|
|
253
|
+
KcError(
|
|
254
|
+
code="KC_VALIDATION_INVALID_ARGUMENT",
|
|
255
|
+
message=f"Unknown output format: {format}",
|
|
256
|
+
details={"requested": format, "supported": ["json", "table", "markdown"]},
|
|
257
|
+
),
|
|
258
|
+
)
|
|
259
|
+
state.format = "json" if is_llm_mode() else format
|
|
260
|
+
state.quiet = quiet or is_llm_mode() or not is_interactive()
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
from kc.commands import artifact, citation, context, doctor, eval, index, source, task # noqa: E402
|
|
264
|
+
from kc.commands import conformance as conformance_command # noqa: E402
|
|
265
|
+
from kc.commands import export as export_command # noqa: E402
|
|
266
|
+
from kc.commands import guide as guide_command # noqa: E402
|
|
267
|
+
from kc.commands import init as init_command # noqa: E402
|
|
268
|
+
from kc.commands import lint as lint_command # noqa: E402
|
|
269
|
+
from kc.commands import status as status_command # noqa: E402
|
|
270
|
+
|
|
271
|
+
guide_command.register(app)
|
|
272
|
+
conformance_command.register(app)
|
|
273
|
+
init_command.register(app)
|
|
274
|
+
status_command.register(app)
|
|
275
|
+
lint_command.register(app)
|
|
276
|
+
export_command.register(app)
|
|
277
|
+
app.add_typer(source.app, name="source")
|
|
278
|
+
app.add_typer(index.app, name="index")
|
|
279
|
+
app.add_typer(context.app, name="context")
|
|
280
|
+
app.add_typer(artifact.app, name="artifact")
|
|
281
|
+
app.add_typer(citation.app, name="citation")
|
|
282
|
+
app.add_typer(task.app, name="task")
|
|
283
|
+
app.add_typer(eval.app, name="eval")
|
|
284
|
+
app.add_typer(doctor.app, name="doctor")
|
kc/commands/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI command modules."""
|