agent-wiki-cli 0.3.28__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.
- agent_wiki_cli-0.3.28.dist-info/METADATA +425 -0
- agent_wiki_cli-0.3.28.dist-info/RECORD +47 -0
- agent_wiki_cli-0.3.28.dist-info/WHEEL +5 -0
- agent_wiki_cli-0.3.28.dist-info/entry_points.txt +2 -0
- agent_wiki_cli-0.3.28.dist-info/licenses/LICENSE +21 -0
- agent_wiki_cli-0.3.28.dist-info/top_level.txt +1 -0
- llm_wiki_cli/__init__.py +7 -0
- llm_wiki_cli/cli.py +231 -0
- llm_wiki_cli/commands/__init__.py +1 -0
- llm_wiki_cli/commands/bootstrap_cmd.py +1072 -0
- llm_wiki_cli/commands/bump_cmd.py +55 -0
- llm_wiki_cli/commands/context_cmd.py +427 -0
- llm_wiki_cli/commands/extract_cmd.py +745 -0
- llm_wiki_cli/commands/generate_prompt_cmd.py +89 -0
- llm_wiki_cli/commands/hook_cmd.py +161 -0
- llm_wiki_cli/commands/init_cmd.py +92 -0
- llm_wiki_cli/commands/lint_cmd.py +294 -0
- llm_wiki_cli/commands/migrate_cmd.py +892 -0
- llm_wiki_cli/commands/release_cmd.py +163 -0
- llm_wiki_cli/commands/status_cmd.py +70 -0
- llm_wiki_cli/commands/sync_cmd.py +521 -0
- llm_wiki_cli/commands/trigger_cmd.py +205 -0
- llm_wiki_cli/commands/uninstall_cmd.py +221 -0
- llm_wiki_cli/commands/upgrade_cmd.py +196 -0
- llm_wiki_cli/config.py +318 -0
- llm_wiki_cli/extractors/__init__.py +46 -0
- llm_wiki_cli/extractors/common.py +90 -0
- llm_wiki_cli/extractors/go_extractor.py +143 -0
- llm_wiki_cli/extractors/go_scripts/go.mod +3 -0
- llm_wiki_cli/extractors/go_scripts/main.go +668 -0
- llm_wiki_cli/extractors/python_extractor.py +346 -0
- llm_wiki_cli/extractors/rust_extractor.py +143 -0
- llm_wiki_cli/extractors/rust_scripts/Cargo.lock +110 -0
- llm_wiki_cli/extractors/rust_scripts/Cargo.toml +11 -0
- llm_wiki_cli/extractors/rust_scripts/src/main.rs +803 -0
- llm_wiki_cli/extractors/ts_extractor.py +206 -0
- llm_wiki_cli/extractors/ts_scripts/extract.js +485 -0
- llm_wiki_cli/extractors/ts_scripts/package.json +10 -0
- llm_wiki_cli/services/__init__.py +0 -0
- llm_wiki_cli/services/circuit_breaker.py +79 -0
- llm_wiki_cli/services/io.py +47 -0
- llm_wiki_cli/services/lockfile.py +60 -0
- llm_wiki_cli/services/packages.py +173 -0
- llm_wiki_cli/services/paths.py +31 -0
- llm_wiki_cli/services/schema.py +214 -0
- llm_wiki_cli/services/secure_file.py +22 -0
- llm_wiki_cli/services/versioning.py +193 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from ..services.lockfile import WikiLock, LockAcquisitionError
|
|
6
|
+
from ..services import circuit_breaker
|
|
7
|
+
from ..services.secure_file import write_private_text
|
|
8
|
+
from ..config import DEFAULT_WIKI_DIR, IDE_AGENTS, validate_path
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
GIT_DIR = Path(".git")
|
|
12
|
+
DEFAULT_MAX_PROMPT_BYTES = 2_000_000
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run(args):
|
|
16
|
+
# Handle --reset-breaker early (no lock needed)
|
|
17
|
+
if getattr(args, "reset_breaker", False):
|
|
18
|
+
circuit_breaker.reset_breaker(GIT_DIR)
|
|
19
|
+
print("Circuit breaker reset. Wiki auto-sync is re-enabled.")
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
if args.agent in IDE_AGENTS:
|
|
23
|
+
print(f"Error: Agent '{args.agent}' is a UI-based assistant for IDEs.")
|
|
24
|
+
print(f"To use background auto-sync, you must specify a CLI-native agent like 'claude' or 'aider'.")
|
|
25
|
+
print(f"Example: llm-wiki trigger-agent --agent claude")
|
|
26
|
+
sys.exit(1)
|
|
27
|
+
|
|
28
|
+
# --- Fuse: Concurrency Lock ---
|
|
29
|
+
try:
|
|
30
|
+
with WikiLock(GIT_DIR):
|
|
31
|
+
_run_sync(args)
|
|
32
|
+
except LockAcquisitionError:
|
|
33
|
+
print("Another llm-wiki sync is already running. Skipping.")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _run_sync(args):
|
|
37
|
+
"""Core sync logic, executed inside the concurrency lock."""
|
|
38
|
+
|
|
39
|
+
wiki_dir = getattr(args, "wiki_dir", DEFAULT_WIKI_DIR)
|
|
40
|
+
validate_path(wiki_dir, "--wiki-dir")
|
|
41
|
+
|
|
42
|
+
# --- Fuse: Circuit Breaker ---
|
|
43
|
+
if circuit_breaker.check_breaker(GIT_DIR):
|
|
44
|
+
print("Circuit breaker is OPEN — wiki auto-sync is disabled after repeated failures.")
|
|
45
|
+
print("To re-enable: llm-wiki trigger-agent --reset-breaker")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
print("Triggering auto-sync workflow for LLM Wiki...")
|
|
49
|
+
|
|
50
|
+
# 1. Get the git diff
|
|
51
|
+
print("Fetching git diff...")
|
|
52
|
+
try:
|
|
53
|
+
git_diff_result = subprocess.run(
|
|
54
|
+
["git", "diff", "HEAD~1..HEAD"],
|
|
55
|
+
capture_output=True, text=True, check=True, timeout=30,
|
|
56
|
+
)
|
|
57
|
+
diff_text = git_diff_result.stdout
|
|
58
|
+
except subprocess.TimeoutExpired:
|
|
59
|
+
print("Git diff timed out (30s). Aborting.")
|
|
60
|
+
return
|
|
61
|
+
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
|
62
|
+
print(f"Git diff failed. Are there commits? {e}")
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
if not diff_text.strip():
|
|
66
|
+
print("No changes in the last commit. Aborting.")
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
# --- Fuse: Diff Size Guard ---
|
|
70
|
+
max_diff = getattr(args, "max_diff_lines", 1000)
|
|
71
|
+
force = getattr(args, "force", False)
|
|
72
|
+
diff_lines = len(diff_text.splitlines())
|
|
73
|
+
if diff_lines > max_diff and not force:
|
|
74
|
+
print(f"Diff too large ({diff_lines} lines > {max_diff} limit). Skipping auto-sync.")
|
|
75
|
+
print("Use --force to override, or increase --max-diff-lines.")
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
# 2. Extract context via current AST
|
|
79
|
+
print("Extracting current structure context...")
|
|
80
|
+
from .extract_cmd import get_call_graph, get_inventory_result, print_inventory_failures
|
|
81
|
+
inventory_result = get_inventory_result(".", deep=True)
|
|
82
|
+
if inventory_result.failed:
|
|
83
|
+
print_inventory_failures(inventory_result)
|
|
84
|
+
return
|
|
85
|
+
inventory = inventory_result.inventory
|
|
86
|
+
ast_json = json.dumps(inventory, indent=2)
|
|
87
|
+
|
|
88
|
+
# 2b. Build call graph for workflow awareness
|
|
89
|
+
call_graph = get_call_graph(inventory)
|
|
90
|
+
graph_json = json.dumps(call_graph, indent=2)
|
|
91
|
+
|
|
92
|
+
# 3. Create context prompt for the subagent
|
|
93
|
+
prompt = f"""
|
|
94
|
+
You are a Wiki synchronizer subagent.
|
|
95
|
+
A new commit was just made. Your task is to update the wiki at `{wiki_dir}/`.
|
|
96
|
+
|
|
97
|
+
## Context
|
|
98
|
+
|
|
99
|
+
AST structure of the codebase:
|
|
100
|
+
{ast_json}
|
|
101
|
+
|
|
102
|
+
Cross-module call graph (functions touching 3+ internal modules):
|
|
103
|
+
{graph_json}
|
|
104
|
+
|
|
105
|
+
Git diff:
|
|
106
|
+
{diff_text}
|
|
107
|
+
|
|
108
|
+
## Success Criteria
|
|
109
|
+
|
|
110
|
+
Your work is done when **all** of the following are true:
|
|
111
|
+
|
|
112
|
+
1. **`llm-wiki lint --wiki-dir {wiki_dir} --src-dir .` exits 0** — no broken links, \
|
|
113
|
+
no orphan pages, no undocumented classes, no stale entities, no missing modules, \
|
|
114
|
+
no broken workflow links, no undocumented infrastructure files.
|
|
115
|
+
2. **Only affected pages changed** — modify wiki pages that correspond to code \
|
|
116
|
+
touched in the diff. Do not edit unrelated pages or reformat existing content.
|
|
117
|
+
3. **`{wiki_dir}/log.md` has a new entry** — one concise line describing what changed, \
|
|
118
|
+
appended at the bottom.
|
|
119
|
+
|
|
120
|
+
## Loop
|
|
121
|
+
|
|
122
|
+
After making your changes, run:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
llm-wiki lint --wiki-dir {wiki_dir} --src-dir .
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
If lint reports issues, fix them and re-run. **Repeat until lint exits 0.** Then commit:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
git add {wiki_dir}/
|
|
132
|
+
LLM_WIKI_AUTO_COMMIT=1 git commit -m "docs(wiki): auto-update [bot]"
|
|
133
|
+
```
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
max_prompt_bytes = _max_prompt_bytes(args)
|
|
137
|
+
prompt_bytes = len(prompt.encode("utf-8"))
|
|
138
|
+
if prompt_bytes > max_prompt_bytes and not force:
|
|
139
|
+
print(
|
|
140
|
+
f"Prompt too large ({prompt_bytes} bytes > {max_prompt_bytes} limit). "
|
|
141
|
+
"Skipping auto-sync."
|
|
142
|
+
)
|
|
143
|
+
print("Use --force to override, or increase --max-prompt-bytes.")
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
# 4. Save the prompt to a temp file
|
|
147
|
+
prompt_file = Path(".git/llm-wiki-prompt.txt")
|
|
148
|
+
write_private_text(prompt_file, prompt)
|
|
149
|
+
|
|
150
|
+
# 5. Delegate to Subagent via CLI
|
|
151
|
+
print(f"Delegating to {args.agent} subagent...")
|
|
152
|
+
if args.agent == "claude":
|
|
153
|
+
cmd = ["claude", "-p", "--dangerously-skip-permissions"]
|
|
154
|
+
elif args.agent == "aider":
|
|
155
|
+
cmd = ["aider", "--message-file", str(prompt_file), "--no-auto-commits"]
|
|
156
|
+
elif args.agent == "opencode":
|
|
157
|
+
cmd = ["opencode", "task", "-f", str(prompt_file)]
|
|
158
|
+
else:
|
|
159
|
+
print(f"Unsupported agent {args.agent}")
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
timeout = getattr(args, "timeout", 300)
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
print(f"Running command: {' '.join(cmd)}")
|
|
166
|
+
|
|
167
|
+
if args.agent == "claude":
|
|
168
|
+
with open(prompt_file, "r", encoding="utf-8") as prompt_in:
|
|
169
|
+
result = subprocess.run(
|
|
170
|
+
cmd, stdin=prompt_in,
|
|
171
|
+
timeout=timeout,
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
with open(os.devnull, "r") as devnull:
|
|
175
|
+
result = subprocess.run(
|
|
176
|
+
cmd, stdin=devnull,
|
|
177
|
+
timeout=timeout,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# --- Fuse: record success / failure based on exit code ---
|
|
181
|
+
if result.returncode != 0:
|
|
182
|
+
print(f"Subagent exited with code {result.returncode}.")
|
|
183
|
+
circuit_breaker.record_failure(GIT_DIR)
|
|
184
|
+
else:
|
|
185
|
+
circuit_breaker.record_success(GIT_DIR)
|
|
186
|
+
|
|
187
|
+
except subprocess.TimeoutExpired:
|
|
188
|
+
print(f"Subagent timed out after {timeout}s. Process killed.")
|
|
189
|
+
circuit_breaker.record_failure(GIT_DIR)
|
|
190
|
+
except Exception as e:
|
|
191
|
+
print(f"Error executing agent {args.agent}: {e}")
|
|
192
|
+
circuit_breaker.record_failure(GIT_DIR)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _max_prompt_bytes(args) -> int:
|
|
196
|
+
value = getattr(args, "max_prompt_bytes", None)
|
|
197
|
+
if value is not None:
|
|
198
|
+
return int(value)
|
|
199
|
+
env_value = os.environ.get("LLM_WIKI_MAX_PROMPT_BYTES")
|
|
200
|
+
if env_value:
|
|
201
|
+
try:
|
|
202
|
+
return int(env_value)
|
|
203
|
+
except ValueError:
|
|
204
|
+
pass
|
|
205
|
+
return DEFAULT_MAX_PROMPT_BYTES
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from ..config import DEFAULT_WIKI_DIR, validate_path
|
|
5
|
+
from ..services.io import read_md, write_md
|
|
6
|
+
from ..services.schema import (
|
|
7
|
+
ALL_SCHEMA_FILES as AGENT_SCHEMA_FILES,
|
|
8
|
+
CONSTRAINT_START,
|
|
9
|
+
CONSTRAINT_END,
|
|
10
|
+
strip_wiki_block as _strip_wiki_block,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
# Hook identifier — all llm-wiki hooks contain this string
|
|
14
|
+
HOOK_SIGNATURE = "LLM Wiki"
|
|
15
|
+
|
|
16
|
+
# Hooks that install-hook may have written
|
|
17
|
+
HOOK_NAMES = ["post-commit", "pre-commit", "pre-push"]
|
|
18
|
+
|
|
19
|
+
# Local runtime artifacts created by init/hooks/trigger-agent.
|
|
20
|
+
RUNTIME_ARTIFACTS = [
|
|
21
|
+
".git/.llm-wiki-agent",
|
|
22
|
+
".git/llm-wiki-prompt.txt",
|
|
23
|
+
".git/llm-wiki.lock",
|
|
24
|
+
".git/llm-wiki-breaker.json",
|
|
25
|
+
".git/llm-wiki-sync.log",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _confirm(prompt: str) -> bool:
|
|
30
|
+
"""Ask for y/n confirmation."""
|
|
31
|
+
try:
|
|
32
|
+
answer = input(f" {prompt} [y/N]: ").strip().lower()
|
|
33
|
+
except (EOFError, KeyboardInterrupt):
|
|
34
|
+
print()
|
|
35
|
+
return False
|
|
36
|
+
return answer in ("y", "yes")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _remove_hooks(dry_run: bool = False) -> int:
|
|
40
|
+
"""Remove llm-wiki hooks, but only if they contain our signature."""
|
|
41
|
+
hooks_dir = Path(".git/hooks")
|
|
42
|
+
removed = 0
|
|
43
|
+
|
|
44
|
+
if not hooks_dir.exists():
|
|
45
|
+
return 0
|
|
46
|
+
|
|
47
|
+
for name in HOOK_NAMES:
|
|
48
|
+
hook_path = hooks_dir / name
|
|
49
|
+
if not hook_path.exists():
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
content = hook_path.read_text(encoding="utf-8")
|
|
53
|
+
if HOOK_SIGNATURE not in content:
|
|
54
|
+
print(f" SKIP hook {name} (not ours — contains custom user content)")
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
if dry_run:
|
|
58
|
+
print(f" WOULD REMOVE hook: {hook_path}")
|
|
59
|
+
else:
|
|
60
|
+
hook_path.unlink()
|
|
61
|
+
print(f" REMOVED hook: {hook_path}")
|
|
62
|
+
removed += 1
|
|
63
|
+
|
|
64
|
+
return removed
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _clean_agent_schemas(dry_run: bool = False) -> int:
|
|
68
|
+
"""Remove the LLM Wiki constraint block from agent schema files.
|
|
69
|
+
|
|
70
|
+
If the file becomes empty after block removal, delete it entirely.
|
|
71
|
+
If user content remains, preserve it.
|
|
72
|
+
"""
|
|
73
|
+
cleaned = 0
|
|
74
|
+
|
|
75
|
+
for filename in AGENT_SCHEMA_FILES:
|
|
76
|
+
schema_path = Path(filename)
|
|
77
|
+
if not schema_path.exists():
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
content = read_md(schema_path)
|
|
81
|
+
if CONSTRAINT_START not in content:
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
stripped = _strip_wiki_block(content)
|
|
85
|
+
|
|
86
|
+
if dry_run:
|
|
87
|
+
if stripped:
|
|
88
|
+
print(f" WOULD CLEAN block from: {filename} (user content preserved)")
|
|
89
|
+
else:
|
|
90
|
+
print(f" WOULD DELETE: {filename} (only contained wiki constraints)")
|
|
91
|
+
else:
|
|
92
|
+
if stripped:
|
|
93
|
+
write_md(schema_path, stripped)
|
|
94
|
+
print(f" CLEANED block from: {filename} (user content preserved)")
|
|
95
|
+
else:
|
|
96
|
+
schema_path.unlink()
|
|
97
|
+
print(f" DELETED: {filename} (only contained wiki constraints)")
|
|
98
|
+
cleaned += 1
|
|
99
|
+
|
|
100
|
+
return cleaned
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _remove_wiki_dir(wiki_dir: Path, dry_run: bool = False) -> bool:
|
|
104
|
+
"""Remove the wiki directory tree."""
|
|
105
|
+
if not wiki_dir.exists():
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
if dry_run:
|
|
109
|
+
page_count = len(list(wiki_dir.rglob("*.md")))
|
|
110
|
+
print(f" WOULD REMOVE: {wiki_dir}/ ({page_count} markdown files)")
|
|
111
|
+
else:
|
|
112
|
+
shutil.rmtree(wiki_dir)
|
|
113
|
+
print(f" REMOVED: {wiki_dir}/")
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _remove_runtime_artifacts(dry_run: bool = False) -> int:
|
|
118
|
+
"""Remove local runtime artifacts created by llm-wiki."""
|
|
119
|
+
removed = 0
|
|
120
|
+
for filepath in RUNTIME_ARTIFACTS:
|
|
121
|
+
p = Path(filepath)
|
|
122
|
+
if p.exists():
|
|
123
|
+
if dry_run:
|
|
124
|
+
print(f" WOULD REMOVE: {filepath}")
|
|
125
|
+
else:
|
|
126
|
+
p.unlink()
|
|
127
|
+
print(f" REMOVED: {filepath}")
|
|
128
|
+
removed += 1
|
|
129
|
+
return removed
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def run(args):
|
|
133
|
+
wiki_dir_arg = getattr(args, "wiki_dir", DEFAULT_WIKI_DIR)
|
|
134
|
+
validate_path(str(wiki_dir_arg), "--wiki-dir")
|
|
135
|
+
wiki_dir = Path(wiki_dir_arg)
|
|
136
|
+
remove_wiki = getattr(args, "remove_wiki", False)
|
|
137
|
+
dry_run = getattr(args, "dry_run", False)
|
|
138
|
+
|
|
139
|
+
if dry_run:
|
|
140
|
+
print("DRY RUN — no files will be modified:\n")
|
|
141
|
+
|
|
142
|
+
# ── 1. Preview what will be removed ──────────────────────────────
|
|
143
|
+
print("LLM Wiki Uninstall")
|
|
144
|
+
print("=" * 40)
|
|
145
|
+
|
|
146
|
+
# Hooks
|
|
147
|
+
print("\n1. Git Hooks:")
|
|
148
|
+
hooks_count = _remove_hooks(dry_run=True)
|
|
149
|
+
if hooks_count == 0:
|
|
150
|
+
print(" Nothing to remove.")
|
|
151
|
+
|
|
152
|
+
# Agent schemas
|
|
153
|
+
print("\n2. Agent Constraint Blocks:")
|
|
154
|
+
schema_count = 0
|
|
155
|
+
for filename in AGENT_SCHEMA_FILES:
|
|
156
|
+
p = Path(filename)
|
|
157
|
+
if p.exists() and CONSTRAINT_START in read_md(p):
|
|
158
|
+
stripped = _strip_wiki_block(read_md(p))
|
|
159
|
+
if stripped:
|
|
160
|
+
print(f" {filename} — will strip wiki block (user content preserved)")
|
|
161
|
+
else:
|
|
162
|
+
print(f" {filename} — will delete (only wiki constraints)")
|
|
163
|
+
schema_count += 1
|
|
164
|
+
if schema_count == 0:
|
|
165
|
+
print(" Nothing to remove.")
|
|
166
|
+
|
|
167
|
+
# Wiki dir
|
|
168
|
+
print("\n3. Wiki Directory:")
|
|
169
|
+
if remove_wiki and wiki_dir.exists():
|
|
170
|
+
page_count = len(list(wiki_dir.rglob("*.md")))
|
|
171
|
+
print(f" {wiki_dir}/ — {page_count} markdown file(s)")
|
|
172
|
+
elif wiki_dir.exists():
|
|
173
|
+
print(f" {wiki_dir}/ — KEPT (use --remove-wiki to delete)")
|
|
174
|
+
else:
|
|
175
|
+
print(" Not found.")
|
|
176
|
+
|
|
177
|
+
# Runtime artifacts
|
|
178
|
+
print("\n4. Runtime Artifacts:")
|
|
179
|
+
artifact_count = sum(1 for f in RUNTIME_ARTIFACTS if Path(f).exists())
|
|
180
|
+
if artifact_count:
|
|
181
|
+
for f in RUNTIME_ARTIFACTS:
|
|
182
|
+
if Path(f).exists():
|
|
183
|
+
print(f" {f}")
|
|
184
|
+
else:
|
|
185
|
+
print(" Nothing to remove.")
|
|
186
|
+
|
|
187
|
+
wiki_targeted = remove_wiki and wiki_dir.exists()
|
|
188
|
+
total = hooks_count + schema_count + (1 if wiki_targeted else 0) + artifact_count
|
|
189
|
+
if total == 0:
|
|
190
|
+
print("\nNothing to uninstall. Project is clean.")
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
if dry_run:
|
|
194
|
+
print(f"\nDry run complete. {total} item(s) would be affected.")
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
# ── 2. Confirm and execute ────────────────────────────────────────
|
|
198
|
+
print(f"\n{total} item(s) will be affected.")
|
|
199
|
+
if not _confirm("Proceed with uninstall?"):
|
|
200
|
+
print("Aborted.")
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
print()
|
|
204
|
+
removed_total = 0
|
|
205
|
+
|
|
206
|
+
# Execute removals
|
|
207
|
+
r = _remove_hooks()
|
|
208
|
+
removed_total += r
|
|
209
|
+
|
|
210
|
+
r = _clean_agent_schemas()
|
|
211
|
+
removed_total += r
|
|
212
|
+
|
|
213
|
+
if remove_wiki and wiki_dir.exists():
|
|
214
|
+
_remove_wiki_dir(wiki_dir)
|
|
215
|
+
removed_total += 1
|
|
216
|
+
|
|
217
|
+
r = _remove_runtime_artifacts()
|
|
218
|
+
removed_total += r
|
|
219
|
+
|
|
220
|
+
print(f"\nUninstall complete. {removed_total} item(s) removed.")
|
|
221
|
+
print("To uninstall the CLI itself: pip uninstall agent-wiki-cli")
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""llm-wiki upgrade — refresh all framework-managed artifacts in place.
|
|
2
|
+
|
|
3
|
+
Replaces the uninstall → init → install-hook cycle with a single idempotent
|
|
4
|
+
command that:
|
|
5
|
+
1. Replaces the agent constraint block with the latest version
|
|
6
|
+
2. Ensures wiki directory structure is complete
|
|
7
|
+
3. Reinstalls git hooks
|
|
8
|
+
4. Optionally switches agents
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import shutil
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from ..config import AGENT_CHOICES, CLI_AGENTS, DEFAULT_WIKI_DIR, IDE_AGENTS, get_agent_config_path, read_config, validate_path, write_config
|
|
18
|
+
from ..services.io import read_md, write_md
|
|
19
|
+
from ..services.schema import (
|
|
20
|
+
ALL_SCHEMA_FILES,
|
|
21
|
+
CONSTRAINT_START,
|
|
22
|
+
SCHEMA_FILENAMES,
|
|
23
|
+
build_schema_content,
|
|
24
|
+
replace_schema_block,
|
|
25
|
+
strip_wiki_block,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Re-use hook builders from hook_cmd to avoid duplication
|
|
29
|
+
from .hook_cmd import _build_ide_post_commit, _build_post_commit, _install_hook
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _read_agent_config(wiki_dir: str) -> str | None:
|
|
33
|
+
"""Read the agent name persisted by `llm-wiki init`."""
|
|
34
|
+
config = read_config(wiki_dir)
|
|
35
|
+
agent = config.get("agent")
|
|
36
|
+
if agent and agent != "generic":
|
|
37
|
+
return agent
|
|
38
|
+
# Check if config file actually exists (defaults return "generic")
|
|
39
|
+
config_path = get_agent_config_path(wiki_dir)
|
|
40
|
+
if config_path.exists():
|
|
41
|
+
return agent
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _resolve_agent(args, wiki_dir: str) -> str:
|
|
46
|
+
"""Resolve agent: CLI --agent flag > persisted config > error."""
|
|
47
|
+
agent = getattr(args, "agent", None)
|
|
48
|
+
if agent:
|
|
49
|
+
return agent
|
|
50
|
+
|
|
51
|
+
stored = _read_agent_config(wiki_dir)
|
|
52
|
+
if stored:
|
|
53
|
+
return stored
|
|
54
|
+
|
|
55
|
+
print(
|
|
56
|
+
"Error: Cannot determine agent.\n"
|
|
57
|
+
f" No --agent flag provided and no config found at .git/.llm-wiki-agent\n\n"
|
|
58
|
+
" Either run `llm-wiki init --agent <agent>` first,\n"
|
|
59
|
+
" or pass --agent to this command:\n"
|
|
60
|
+
f" llm-wiki upgrade --agent <{'|'.join(AGENT_CHOICES)}>",
|
|
61
|
+
file=sys.stderr,
|
|
62
|
+
)
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _upgrade_schema(agent: str, wiki_dir: str, old_agent: str | None, *, quality_hints: bool = True) -> str:
|
|
67
|
+
"""Replace or migrate the agent schema constraint block.
|
|
68
|
+
|
|
69
|
+
Returns a summary message.
|
|
70
|
+
"""
|
|
71
|
+
new_content = build_schema_content(agent, wiki_dir, quality_hints=quality_hints)
|
|
72
|
+
new_filename = SCHEMA_FILENAMES.get(agent)
|
|
73
|
+
|
|
74
|
+
if old_agent and old_agent != agent:
|
|
75
|
+
# Switching agents — clean old schema file first
|
|
76
|
+
old_filename = SCHEMA_FILENAMES.get(old_agent)
|
|
77
|
+
if old_filename:
|
|
78
|
+
old_path = Path(old_filename)
|
|
79
|
+
if old_path.exists():
|
|
80
|
+
existing = read_md(old_path)
|
|
81
|
+
if CONSTRAINT_START in existing:
|
|
82
|
+
stripped = strip_wiki_block(existing)
|
|
83
|
+
if stripped:
|
|
84
|
+
write_md(old_path, stripped)
|
|
85
|
+
print(f" Cleaned constraint block from: {old_filename}")
|
|
86
|
+
else:
|
|
87
|
+
old_path.unlink()
|
|
88
|
+
print(f" Removed: {old_filename} (only contained wiki constraints)")
|
|
89
|
+
|
|
90
|
+
# Write latest block to the target schema file
|
|
91
|
+
if new_filename:
|
|
92
|
+
schema_path = Path(new_filename)
|
|
93
|
+
replace_schema_block(schema_path, new_content)
|
|
94
|
+
return new_filename
|
|
95
|
+
return "(no schema file)"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _upgrade_dirs(wiki_dir: str) -> int:
|
|
99
|
+
"""Ensure all standard wiki subdirectories exist. Returns count of newly created dirs."""
|
|
100
|
+
base = Path(wiki_dir)
|
|
101
|
+
subdirs = ["entities", "modules", "workflows", "infrastructure"]
|
|
102
|
+
created = 0
|
|
103
|
+
for name in ["."] + subdirs:
|
|
104
|
+
d = base if name == "." else base / name
|
|
105
|
+
if not d.exists():
|
|
106
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
created += 1
|
|
108
|
+
gitkeep = d / ".gitkeep"
|
|
109
|
+
if not gitkeep.exists():
|
|
110
|
+
gitkeep.touch()
|
|
111
|
+
# Ensure core files exist
|
|
112
|
+
index_path = base / "index.md"
|
|
113
|
+
if not index_path.exists():
|
|
114
|
+
write_md(index_path,
|
|
115
|
+
"# LLM Wiki Index\n\nCatalog of project modules and entities.\n\n"
|
|
116
|
+
"## Entities\n\n## Modules\n\n## Workflows\n\n## Infrastructure\n"
|
|
117
|
+
)
|
|
118
|
+
created += 1
|
|
119
|
+
log_path = base / "log.md"
|
|
120
|
+
if not log_path.exists():
|
|
121
|
+
write_md(log_path, "# Architectural Log\n\nAppend-only chronological log.\n\n")
|
|
122
|
+
created += 1
|
|
123
|
+
return created
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _upgrade_hooks(agent: str, wiki_dir: str, *, force: bool = False) -> None:
|
|
127
|
+
"""Reinstall git hooks for the resolved agent."""
|
|
128
|
+
git_dir = Path(".git")
|
|
129
|
+
if not git_dir.exists():
|
|
130
|
+
print(" Skipped hooks (no .git directory)")
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
hooks_dir = git_dir / "hooks"
|
|
134
|
+
hooks_dir.mkdir(exist_ok=True)
|
|
135
|
+
|
|
136
|
+
if agent in IDE_AGENTS:
|
|
137
|
+
_install_hook(hooks_dir, "post-commit", _build_ide_post_commit(wiki_dir), force=force)
|
|
138
|
+
print(f" Hooks: IDE prompt-generation mode ({agent})")
|
|
139
|
+
else:
|
|
140
|
+
_install_hook(hooks_dir, "post-commit", _build_post_commit(agent, wiki_dir), force=force)
|
|
141
|
+
print(f" Hooks: CLI auto-sync mode ({agent})")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def run(args):
|
|
145
|
+
wiki_dir = getattr(args, "wiki_dir", DEFAULT_WIKI_DIR)
|
|
146
|
+
validate_path(wiki_dir, "--wiki-dir")
|
|
147
|
+
|
|
148
|
+
agent = _resolve_agent(args, wiki_dir)
|
|
149
|
+
old_agent = _read_agent_config(wiki_dir)
|
|
150
|
+
switching = old_agent and old_agent != agent
|
|
151
|
+
|
|
152
|
+
# Resolve quality_hints: CLI flag > stored config > default (True)
|
|
153
|
+
cli_hints = getattr(args, "quality_hints", None)
|
|
154
|
+
if cli_hints is not None:
|
|
155
|
+
quality_hints = cli_hints
|
|
156
|
+
else:
|
|
157
|
+
stored = read_config(wiki_dir)
|
|
158
|
+
quality_hints = stored.get("quality_hints", True)
|
|
159
|
+
|
|
160
|
+
print("LLM Wiki Upgrade")
|
|
161
|
+
print("=" * 40)
|
|
162
|
+
|
|
163
|
+
if switching:
|
|
164
|
+
print(f"\n Switching agent: {old_agent} → {agent}")
|
|
165
|
+
else:
|
|
166
|
+
print(f"\n Agent: {agent}")
|
|
167
|
+
|
|
168
|
+
# 1. Schema constraint block
|
|
169
|
+
print("\n1. Agent Schema:")
|
|
170
|
+
schema_file = _upgrade_schema(agent, wiki_dir, old_agent, quality_hints=quality_hints)
|
|
171
|
+
print(f" Updated: {schema_file}")
|
|
172
|
+
|
|
173
|
+
# 2. Wiki directories
|
|
174
|
+
print("\n2. Wiki Structure:")
|
|
175
|
+
new_dirs = _upgrade_dirs(wiki_dir)
|
|
176
|
+
if new_dirs:
|
|
177
|
+
print(f" Created {new_dirs} new entries in {wiki_dir}/")
|
|
178
|
+
else:
|
|
179
|
+
print(f" All directories present in {wiki_dir}/")
|
|
180
|
+
|
|
181
|
+
# 3. Git hooks
|
|
182
|
+
print("\n3. Git Hooks:")
|
|
183
|
+
_upgrade_hooks(agent, wiki_dir, force=getattr(args, "force", False))
|
|
184
|
+
|
|
185
|
+
# 4. Persist agent config
|
|
186
|
+
write_config(wiki_dir, {"agent": agent, "quality_hints": quality_hints})
|
|
187
|
+
|
|
188
|
+
# Warn if CLI agent executable missing
|
|
189
|
+
executable = CLI_AGENTS.get(agent)
|
|
190
|
+
if executable and not shutil.which(executable):
|
|
191
|
+
print(
|
|
192
|
+
f"\nWarning: '{executable}' not found on PATH.\n"
|
|
193
|
+
f" Background auto-sync won't work until '{executable}' is installed."
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
print("\nUpgrade complete.")
|