cli-wikia 0.10.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.
- cli_wikia/__init__.py +5 -0
- cli_wikia/cli.py +456 -0
- cli_wikia/hooks.py +310 -0
- cli_wikia/schedule.py +206 -0
- cli_wikia/wikis/antigravity/README.md +142 -0
- cli_wikia/wikis/antigravity/agentic-model.md +130 -0
- cli_wikia/wikis/antigravity/cli-reference.md +184 -0
- cli_wikia/wikis/antigravity/cli-vs-api.md +33 -0
- cli_wikia/wikis/antigravity/configuration.md +124 -0
- cli_wikia/wikis/antigravity/customization.md +119 -0
- cli_wikia/wikis/antigravity/hooks.md +64 -0
- cli_wikia/wikis/antigravity/ide-and-app.md +110 -0
- cli_wikia/wikis/antigravity/mcp.md +98 -0
- cli_wikia/wikis/antigravity/models.md +92 -0
- cli_wikia/wikis/antigravity/overview.md +80 -0
- cli_wikia/wikis/antigravity/permissions.md +134 -0
- cli_wikia/wikis/antigravity/plugins.md +115 -0
- cli_wikia/wikis/antigravity/projects-sessions-conversations.md +111 -0
- cli_wikia/wikis/antigravity/sandbox.md +90 -0
- cli_wikia/wikis/antigravity/sdk.md +120 -0
- cli_wikia/wikis/chatgpt/README.md +80 -0
- cli_wikia/wikis/chatgpt/cli-reference.md +90 -0
- cli_wikia/wikis/chatgpt/cli-vs-api.md +34 -0
- cli_wikia/wikis/chatgpt/codex-agents-md.md +90 -0
- cli_wikia/wikis/chatgpt/codex-approvals-sandbox.md +136 -0
- cli_wikia/wikis/chatgpt/codex-auth.md +111 -0
- cli_wikia/wikis/chatgpt/codex-cli-reference.md +136 -0
- cli_wikia/wikis/chatgpt/codex-config.md +276 -0
- cli_wikia/wikis/chatgpt/codex-exec.md +104 -0
- cli_wikia/wikis/chatgpt/codex-mcp.md +114 -0
- cli_wikia/wikis/chatgpt/codex-models.md +82 -0
- cli_wikia/wikis/chatgpt/codex-overview.md +136 -0
- cli_wikia/wikis/chatgpt/codex-slash-commands.md +97 -0
- cli_wikia/wikis/chatgpt/configuration.md +55 -0
- cli_wikia/wikis/chatgpt/models.md +43 -0
- cli_wikia/wikis/claude/README.md +163 -0
- cli_wikia/wikis/claude/advisor.md +127 -0
- cli_wikia/wikis/claude/agent-teams.md +310 -0
- cli_wikia/wikis/claude/agents.md +329 -0
- cli_wikia/wikis/claude/channels.md +193 -0
- cli_wikia/wikis/claude/claude-code-web.md +317 -0
- cli_wikia/wikis/claude/cli-reference.md +240 -0
- cli_wikia/wikis/claude/cli-vs-api.md +27 -0
- cli_wikia/wikis/claude/environment-variables.md +176 -0
- cli_wikia/wikis/claude/headless-sdk.md +206 -0
- cli_wikia/wikis/claude/hooks.md +380 -0
- cli_wikia/wikis/claude/ide-integrations.md +117 -0
- cli_wikia/wikis/claude/marketplaces.md +133 -0
- cli_wikia/wikis/claude/mcp.md +369 -0
- cli_wikia/wikis/claude/memory.md +193 -0
- cli_wikia/wikis/claude/models.md +179 -0
- cli_wikia/wikis/claude/monitors.md +121 -0
- cli_wikia/wikis/claude/output-styles.md +110 -0
- cli_wikia/wikis/claude/permission-modes.md +219 -0
- cli_wikia/wikis/claude/permissions.md +249 -0
- cli_wikia/wikis/claude/plugins.md +335 -0
- cli_wikia/wikis/claude/remote-control.md +169 -0
- cli_wikia/wikis/claude/sandboxing.md +259 -0
- cli_wikia/wikis/claude/settings.md +342 -0
- cli_wikia/wikis/claude/skills.md +338 -0
- cli_wikia/wikis/claude/slash-commands.md +150 -0
- cli_wikia/wikis/claude/stacking.md +301 -0
- cli_wikia/wikis/claude/statusline.md +109 -0
- cli_wikia/wikis/copilot/README.md +109 -0
- cli_wikia/wikis/copilot/billing.md +48 -0
- cli_wikia/wikis/copilot/cli-reference.md +193 -0
- cli_wikia/wikis/copilot/cli-vs-api.md +32 -0
- cli_wikia/wikis/copilot/configuration.md +140 -0
- cli_wikia/wikis/copilot/custom-agents.md +95 -0
- cli_wikia/wikis/copilot/custom-instructions.md +96 -0
- cli_wikia/wikis/copilot/environment-variables.md +100 -0
- cli_wikia/wikis/copilot/getting-started.md +117 -0
- cli_wikia/wikis/copilot/hooks.md +50 -0
- cli_wikia/wikis/copilot/logging.md +56 -0
- cli_wikia/wikis/copilot/mcp.md +127 -0
- cli_wikia/wikis/copilot/models.md +77 -0
- cli_wikia/wikis/copilot/modes.md +80 -0
- cli_wikia/wikis/copilot/monitoring.md +94 -0
- cli_wikia/wikis/copilot/permissions.md +112 -0
- cli_wikia/wikis/copilot/plugins.md +83 -0
- cli_wikia/wikis/copilot/providers-byok.md +89 -0
- cli_wikia/wikis/copilot/sessions.md +93 -0
- cli_wikia/wikis/copilot/skills.md +59 -0
- cli_wikia/wikis/copilot/slash-commands.md +112 -0
- cli_wikia/wikis/deepseek/README.md +137 -0
- cli_wikia/wikis/deepseek/agents.md +203 -0
- cli_wikia/wikis/deepseek/architecture.md +205 -0
- cli_wikia/wikis/deepseek/cli-reference.md +182 -0
- cli_wikia/wikis/deepseek/cli-vs-api.md +28 -0
- cli_wikia/wikis/deepseek/configuration.md +175 -0
- cli_wikia/wikis/deepseek/hooks.md +232 -0
- cli_wikia/wikis/deepseek/models.md +136 -0
- cli_wikia/wikis/deepseek/permissions.md +160 -0
- cli_wikia/wikis/deepseek/plugins.md +107 -0
- cli_wikia/wikis/deepseek/sessions.md +128 -0
- cli_wikia/wikis/deepseek/skills.md +184 -0
- cli_wikia/wikis/gemini/README.md +161 -0
- cli_wikia/wikis/gemini/checkpointing.md +104 -0
- cli_wikia/wikis/gemini/cli-reference.md +201 -0
- cli_wikia/wikis/gemini/cli-vs-api.md +27 -0
- cli_wikia/wikis/gemini/commands.md +150 -0
- cli_wikia/wikis/gemini/configuration.md +129 -0
- cli_wikia/wikis/gemini/context-files.md +191 -0
- cli_wikia/wikis/gemini/custom-commands.md +169 -0
- cli_wikia/wikis/gemini/enterprise.md +395 -0
- cli_wikia/wikis/gemini/environment-variables.md +142 -0
- cli_wikia/wikis/gemini/extensions.md +305 -0
- cli_wikia/wikis/gemini/getting-started.md +145 -0
- cli_wikia/wikis/gemini/git-worktrees.md +74 -0
- cli_wikia/wikis/gemini/headless.md +208 -0
- cli_wikia/wikis/gemini/hooks.md +448 -0
- cli_wikia/wikis/gemini/ide-integration.md +129 -0
- cli_wikia/wikis/gemini/mcp.md +484 -0
- cli_wikia/wikis/gemini/models.md +267 -0
- cli_wikia/wikis/gemini/permissions.md +252 -0
- cli_wikia/wikis/gemini/sandboxing.md +275 -0
- cli_wikia/wikis/gemini/sessions.md +161 -0
- cli_wikia/wikis/gemini/settings.md +238 -0
- cli_wikia/wikis/gemini/skills.md +228 -0
- cli_wikia/wikis/gemini/subagents.md +424 -0
- cli_wikia/wikis/gemini/themes.md +204 -0
- cli_wikia/wikis/gemini/tools.md +468 -0
- cli_wikia-0.10.0.dist-info/METADATA +90 -0
- cli_wikia-0.10.0.dist-info/RECORD +127 -0
- cli_wikia-0.10.0.dist-info/WHEEL +4 -0
- cli_wikia-0.10.0.dist-info/entry_points.txt +2 -0
- cli_wikia-0.10.0.dist-info/licenses/LICENSE +21 -0
cli_wikia/__init__.py
ADDED
cli_wikia/cli.py
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
"""Command-line interface for cli-wikia.
|
|
2
|
+
|
|
3
|
+
A small, dependency-free CLI to browse, search, read and edit an offline
|
|
4
|
+
reference wiki for AI coding CLIs. Run `wikia --help` for usage.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import difflib
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import urllib.request
|
|
16
|
+
from importlib import resources
|
|
17
|
+
|
|
18
|
+
from . import MODELS, __version__
|
|
19
|
+
|
|
20
|
+
# Which installed CLI can answer questions / provide updates for each model.
|
|
21
|
+
MODEL_CLIS = {
|
|
22
|
+
"claude": "claude",
|
|
23
|
+
"deepseek": "deepseek-code",
|
|
24
|
+
"copilot": "copilot",
|
|
25
|
+
"chatgpt": "codex", # OpenAI Codex CLI (may not be installed yet)
|
|
26
|
+
"gemini": "gemini",
|
|
27
|
+
"antigravity": "agy", # Google Antigravity CLI
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Sources the `update` command checks for changes, per model. The real signal
|
|
31
|
+
# comes from (1) the official docs and (2) asking the model itself — NOT a
|
|
32
|
+
# `--help` dump. `version` is just a cheap authoritative version string.
|
|
33
|
+
# - version: read-only version probe (no side effects).
|
|
34
|
+
# - docs: official documentation URL (best-effort; edit if a tool moves its docs).
|
|
35
|
+
# - ask: argv template to query the model in one-shot mode; "{q}" = the question.
|
|
36
|
+
# None means the model can't be queried that way (e.g. tool not installed).
|
|
37
|
+
MODEL_SOURCES = {
|
|
38
|
+
"claude": {
|
|
39
|
+
"version": ["--version"],
|
|
40
|
+
"docs": "https://docs.claude.com/en/docs/claude-code/overview",
|
|
41
|
+
"ask": ["-p", "{q}"],
|
|
42
|
+
},
|
|
43
|
+
"deepseek": {
|
|
44
|
+
"version": ["--version"],
|
|
45
|
+
"docs": "https://api-docs.deepseek.com/",
|
|
46
|
+
"ask": ["-p", "{q}"],
|
|
47
|
+
},
|
|
48
|
+
"copilot": {
|
|
49
|
+
"version": ["--version"],
|
|
50
|
+
"docs": "https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli",
|
|
51
|
+
"ask": ["-p", "{q}", "--allow-all-tools"],
|
|
52
|
+
},
|
|
53
|
+
"chatgpt": {
|
|
54
|
+
"version": ["--version"],
|
|
55
|
+
"docs": "https://developers.openai.com/codex/cli/",
|
|
56
|
+
"ask": None, # codex isn't installed; openai CLI isn't a one-shot agent
|
|
57
|
+
},
|
|
58
|
+
"gemini": {
|
|
59
|
+
"version": ["--version"],
|
|
60
|
+
"docs": "https://google-gemini.github.io/gemini-cli/",
|
|
61
|
+
"ask": ["-p", "{q}"],
|
|
62
|
+
},
|
|
63
|
+
"antigravity": {
|
|
64
|
+
"version": ["--version"],
|
|
65
|
+
"docs": "https://antigravity.google/docs",
|
|
66
|
+
"ask": ["-p", "{q}"],
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# What `update` asks each model about itself.
|
|
71
|
+
WHATS_NEW_Q = (
|
|
72
|
+
"What is your exact version, and what are your most recent features, "
|
|
73
|
+
"commands, or changes? Be specific and concise."
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def wikis_root():
|
|
78
|
+
"""Filesystem path to the bundled wikis/ directory."""
|
|
79
|
+
return resources.files("cli_wikia") / "wikis"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def model_dir(model):
|
|
83
|
+
return wikis_root() / model
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def topics(model):
|
|
87
|
+
"""Sorted list of topic names (filenames without .md) for a model."""
|
|
88
|
+
d = model_dir(model)
|
|
89
|
+
if not d.is_dir():
|
|
90
|
+
return []
|
|
91
|
+
return sorted(p.name[:-3] for p in d.iterdir() if p.name.endswith(".md"))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def resolve_model(model):
|
|
95
|
+
if model not in MODELS:
|
|
96
|
+
sys.exit(f"unknown model '{model}'. choose from: {', '.join(MODELS)}")
|
|
97
|
+
return model
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# --------------------------------------------------------------------------- #
|
|
101
|
+
# commands
|
|
102
|
+
# --------------------------------------------------------------------------- #
|
|
103
|
+
def cmd_models(args):
|
|
104
|
+
for m in MODELS:
|
|
105
|
+
n = len(topics(m))
|
|
106
|
+
cli = MODEL_CLIS.get(m)
|
|
107
|
+
cli_state = f"cli: {cli}" if cli and shutil.which(cli) else "cli: not installed"
|
|
108
|
+
print(f"{m:12} {n:3} topics ({cli_state})")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def cmd_list(args):
|
|
112
|
+
models = [resolve_model(args.model)] if args.model else MODELS
|
|
113
|
+
for m in models:
|
|
114
|
+
ts = topics(m)
|
|
115
|
+
print(f"\n# {m} ({len(ts)} topics)")
|
|
116
|
+
for t in ts:
|
|
117
|
+
print(f" {t}")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def cmd_read(args):
|
|
121
|
+
m = resolve_model(args.model)
|
|
122
|
+
f = model_dir(m) / f"{args.topic}.md"
|
|
123
|
+
if not f.is_file():
|
|
124
|
+
avail = ", ".join(topics(m)) or "(none yet)"
|
|
125
|
+
sys.exit(f"no topic '{args.topic}' in {m}.\navailable: {avail}")
|
|
126
|
+
sys.stdout.write(f.read_text(encoding="utf-8"))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def cmd_search(args):
|
|
130
|
+
needle = args.query.lower()
|
|
131
|
+
models = [resolve_model(args.model)] if args.model else MODELS
|
|
132
|
+
hits = 0
|
|
133
|
+
for m in models:
|
|
134
|
+
d = model_dir(m)
|
|
135
|
+
if not d.is_dir():
|
|
136
|
+
continue
|
|
137
|
+
for p in sorted(d.iterdir(), key=lambda x: x.name):
|
|
138
|
+
if not p.name.endswith(".md"):
|
|
139
|
+
continue
|
|
140
|
+
for i, line in enumerate(p.read_text(encoding="utf-8").splitlines(), 1):
|
|
141
|
+
if needle in line.lower():
|
|
142
|
+
hits += 1
|
|
143
|
+
print(f"{m}/{p.name[:-3]}:{i}: {line.strip()}")
|
|
144
|
+
if not hits:
|
|
145
|
+
print(f"no matches for '{args.query}'")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def cmd_path(args):
|
|
149
|
+
if args.model:
|
|
150
|
+
print(model_dir(resolve_model(args.model)))
|
|
151
|
+
else:
|
|
152
|
+
print(wikis_root())
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def cmd_ask(args):
|
|
156
|
+
"""Use a local model CLI to answer a question grounded in the wiki docs."""
|
|
157
|
+
m = resolve_model(args.model)
|
|
158
|
+
cli = MODEL_CLIS.get(m)
|
|
159
|
+
runner = cli if (cli and shutil.which(cli)) else ("ollama" if shutil.which("ollama") else None)
|
|
160
|
+
if not runner:
|
|
161
|
+
sys.exit(
|
|
162
|
+
"no local model CLI available to answer. install one of: "
|
|
163
|
+
f"{cli or 'ollama'}, or read the docs with `wikia read {m} <topic>`."
|
|
164
|
+
)
|
|
165
|
+
# Build context from the model's docs (capped to keep the prompt sane).
|
|
166
|
+
context = ""
|
|
167
|
+
for t in topics(m):
|
|
168
|
+
context += f"\n\n## {t}\n" + (model_dir(m) / f"{t}.md").read_text(encoding="utf-8")
|
|
169
|
+
context = context[: args.max_context]
|
|
170
|
+
prompt = (
|
|
171
|
+
"Answer the question using ONLY the reference docs below. "
|
|
172
|
+
"If the answer is not in them, say so.\n"
|
|
173
|
+
f"=== REFERENCE DOCS ({m}) ===\n{context}\n=== QUESTION ===\n{args.question}\n"
|
|
174
|
+
)
|
|
175
|
+
print(f"(asking via: {runner})\n", file=sys.stderr)
|
|
176
|
+
try:
|
|
177
|
+
if runner == "ollama":
|
|
178
|
+
subprocess.run(["ollama", "run", args.ollama_model, prompt], check=False)
|
|
179
|
+
else:
|
|
180
|
+
subprocess.run([runner, prompt], check=False)
|
|
181
|
+
except FileNotFoundError:
|
|
182
|
+
sys.exit(f"could not run '{runner}'.")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def snapshot_dir():
|
|
186
|
+
"""Writable per-user dir where CLI ground-truth snapshots are stored."""
|
|
187
|
+
base = os.environ.get("XDG_STATE_HOME") or os.path.join(
|
|
188
|
+
os.path.expanduser("~"), ".local", "state"
|
|
189
|
+
)
|
|
190
|
+
return os.path.join(base, "cli-wikia", "snapshots")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _run_cli(cli, probe, timeout=30):
|
|
194
|
+
"""Run one read-only CLI probe and return its labelled output."""
|
|
195
|
+
try:
|
|
196
|
+
r = subprocess.run(
|
|
197
|
+
[cli, *probe],
|
|
198
|
+
capture_output=True,
|
|
199
|
+
text=True,
|
|
200
|
+
timeout=timeout,
|
|
201
|
+
stdin=subprocess.DEVNULL,
|
|
202
|
+
)
|
|
203
|
+
return f"$ {cli} {' '.join(probe)}\n{r.stdout}{r.stderr}".strip()
|
|
204
|
+
except (subprocess.TimeoutExpired, OSError) as e:
|
|
205
|
+
return f"$ {cli} {' '.join(probe)}\n<error: {e}>"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def fetch_docs(url):
|
|
209
|
+
"""Fetch an official docs page and reduce it to text for change detection.
|
|
210
|
+
|
|
211
|
+
Uses only the standard library. Returns a text block, or an error note if
|
|
212
|
+
offline / the page is unreachable (so update still works offline).
|
|
213
|
+
"""
|
|
214
|
+
try:
|
|
215
|
+
req = urllib.request.Request(url, headers={"User-Agent": "cli-wikia/update"})
|
|
216
|
+
with urllib.request.urlopen(req, timeout=20) as resp:
|
|
217
|
+
html = resp.read(500_000).decode("utf-8", "replace")
|
|
218
|
+
except Exception as e: # noqa: BLE001 - network can fail many ways; degrade gracefully
|
|
219
|
+
return f"# docs: {url}\n<unreachable: {e}>"
|
|
220
|
+
text = re.sub(r"(?is)<(script|style).*?</\1>", " ", html)
|
|
221
|
+
text = re.sub(r"(?s)<[^>]+>", " ", text)
|
|
222
|
+
text = re.sub(r"\s+", " ", text).strip()
|
|
223
|
+
return f"# docs: {url}\n{text[:30000]}"
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def wiki_doc_urls(model, limit=2):
|
|
227
|
+
"""Official doc URLs for a model, discovered FROM its own wiki pages
|
|
228
|
+
(dynamic — not a hardcoded list). Prefers documentation-looking links."""
|
|
229
|
+
d = model_dir(model)
|
|
230
|
+
if not d.is_dir():
|
|
231
|
+
return []
|
|
232
|
+
text = "".join(
|
|
233
|
+
p.read_text(encoding="utf-8") for p in sorted(d.iterdir()) if p.name.endswith(".md")
|
|
234
|
+
)
|
|
235
|
+
urls = [u.rstrip(".,);]`") for u in re.findall(r"https?://[^\s)\]\"'>`]+", text)]
|
|
236
|
+
bad = ("example.com", "example.org", "localhost", "127.0.0.1", "your-", "git-scm.com")
|
|
237
|
+
urls = [u for u in urls if not any(b in u for b in bad)]
|
|
238
|
+
docish = [u for u in urls if re.search(r"docs?[./]|/docs|developers?\.|\.github\.io|/guide", u)]
|
|
239
|
+
out = []
|
|
240
|
+
for u in (docish or urls):
|
|
241
|
+
if u not in out:
|
|
242
|
+
out.append(u)
|
|
243
|
+
if len(out) >= limit:
|
|
244
|
+
break
|
|
245
|
+
return out
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def query_model(cli, ask_template, question):
|
|
249
|
+
"""Ask the model itself, in one-shot mode, via its per-model invocation."""
|
|
250
|
+
if not ask_template:
|
|
251
|
+
return None
|
|
252
|
+
argv = [question if tok == "{q}" else tok for tok in ask_template]
|
|
253
|
+
out = _run_cli(cli, argv, timeout=180) # models take a while
|
|
254
|
+
return "# model self-report (the model's own answer about itself):\n" + out
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def capture_sources(m, cli, use_docs, use_model):
|
|
258
|
+
"""Gather sources for a model into one snapshot blob, leaning on the docs.
|
|
259
|
+
Order = priority: (1) OFFICIAL DOCUMENTATION first (URLs discovered from the
|
|
260
|
+
model's own wiki, plus a seed), (2) the CLI's own facts (version + help),
|
|
261
|
+
(3) the model's self-report (secondary — models are unreliable about
|
|
262
|
+
themselves). Use --no-docs / --no-model to drop a source."""
|
|
263
|
+
src = MODEL_SOURCES.get(m, {})
|
|
264
|
+
parts = []
|
|
265
|
+
if use_docs:
|
|
266
|
+
urls = wiki_doc_urls(m)
|
|
267
|
+
seed = src.get("docs")
|
|
268
|
+
if seed and seed not in urls:
|
|
269
|
+
urls.append(seed)
|
|
270
|
+
for url in urls[:2]:
|
|
271
|
+
parts.append(fetch_docs(url))
|
|
272
|
+
parts.append(_run_cli(cli, src.get("version", ["--version"])))
|
|
273
|
+
parts.append(_run_cli(cli, ["--help"]))
|
|
274
|
+
if use_model and src.get("ask"):
|
|
275
|
+
mq = query_model(cli, src["ask"], WHATS_NEW_Q)
|
|
276
|
+
if mq:
|
|
277
|
+
parts.append(mq)
|
|
278
|
+
return "\n\n".join(p for p in parts if p) + "\n"
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def cmd_update(args):
|
|
282
|
+
"""Check each model's sources (CLI --help/--version, official docs, and
|
|
283
|
+
optionally the model itself) for changes vs the last saved snapshot.
|
|
284
|
+
|
|
285
|
+
Reports what changed so curated docs can be refreshed. No API keys. Never
|
|
286
|
+
overwrites the curated .md files; snapshots live in the user state dir.
|
|
287
|
+
"""
|
|
288
|
+
if not args.all and not args.model:
|
|
289
|
+
sys.exit("specify a model (e.g. `wikia update gemini`) or use `--all`.")
|
|
290
|
+
models = MODELS if args.all else [resolve_model(args.model)]
|
|
291
|
+
use_docs = not args.no_docs
|
|
292
|
+
use_model = not args.no_model
|
|
293
|
+
sdir = snapshot_dir()
|
|
294
|
+
os.makedirs(sdir, exist_ok=True)
|
|
295
|
+
changed_any = False
|
|
296
|
+
for m in models:
|
|
297
|
+
cli = MODEL_CLIS.get(m)
|
|
298
|
+
if not cli:
|
|
299
|
+
print(f"{m:12} no associated CLI — skip")
|
|
300
|
+
continue
|
|
301
|
+
if not shutil.which(cli):
|
|
302
|
+
print(f"{m:12} '{cli}' not installed — can't check for updates")
|
|
303
|
+
continue
|
|
304
|
+
sources = "help/version" + (" + docs" if use_docs else "") + (" + model" if use_model else "")
|
|
305
|
+
current = capture_sources(m, cli, use_docs, use_model)
|
|
306
|
+
snap = os.path.join(sdir, f"{m}.txt")
|
|
307
|
+
if not os.path.exists(snap):
|
|
308
|
+
with open(snap, "w", encoding="utf-8") as f:
|
|
309
|
+
f.write(current)
|
|
310
|
+
print(f"{m:12} baseline snapshot saved ({sources}). Run again later to detect changes.")
|
|
311
|
+
continue
|
|
312
|
+
with open(snap, encoding="utf-8") as f:
|
|
313
|
+
prev = f.read()
|
|
314
|
+
if prev == current:
|
|
315
|
+
print(f"{m:12} up to date ({sources}, no change)")
|
|
316
|
+
continue
|
|
317
|
+
changed_any = True
|
|
318
|
+
diff = [
|
|
319
|
+
ln
|
|
320
|
+
for ln in difflib.unified_diff(
|
|
321
|
+
prev.splitlines(), current.splitlines(), lineterm="", n=0
|
|
322
|
+
)
|
|
323
|
+
if ln and ln[0] in "+-" and not ln.startswith(("+++", "---"))
|
|
324
|
+
]
|
|
325
|
+
print(f"{m:12} CHANGED ({sources}) — {len(diff)} differing lines:")
|
|
326
|
+
for ln in diff[:30]:
|
|
327
|
+
print(f" {ln}")
|
|
328
|
+
if len(diff) > 30:
|
|
329
|
+
print(f" … (+{len(diff) - 30} more)")
|
|
330
|
+
print(f" review/update curated docs in: {model_dir(m)}")
|
|
331
|
+
if args.write:
|
|
332
|
+
with open(snap, "w", encoding="utf-8") as f:
|
|
333
|
+
f.write(current)
|
|
334
|
+
print(f" snapshot updated (acknowledged).")
|
|
335
|
+
else:
|
|
336
|
+
print(f" re-run with --write to accept this as the new baseline.")
|
|
337
|
+
if changed_any and not args.write:
|
|
338
|
+
print("\nTip: `wikia update --all --write` after you've refreshed the docs.")
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def build_parser():
|
|
342
|
+
p = argparse.ArgumentParser(
|
|
343
|
+
prog="wikia",
|
|
344
|
+
description="Offline reference wiki for AI coding CLIs "
|
|
345
|
+
"(claude, deepseek, copilot, chatgpt, gemini).",
|
|
346
|
+
)
|
|
347
|
+
p.add_argument("--version", action="version", version=f"cli-wikia {__version__}")
|
|
348
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
349
|
+
|
|
350
|
+
sub.add_parser("models", help="list models and how many topics each has").set_defaults(func=cmd_models)
|
|
351
|
+
|
|
352
|
+
sp = sub.add_parser("list", help="list topics (optionally for one model)")
|
|
353
|
+
sp.add_argument("model", nargs="?", help="model name (default: all)")
|
|
354
|
+
sp.set_defaults(func=cmd_list)
|
|
355
|
+
|
|
356
|
+
sp = sub.add_parser("read", help="print a topic")
|
|
357
|
+
sp.add_argument("model")
|
|
358
|
+
sp.add_argument("topic")
|
|
359
|
+
sp.set_defaults(func=cmd_read)
|
|
360
|
+
|
|
361
|
+
sp = sub.add_parser("search", help="search text across topics")
|
|
362
|
+
sp.add_argument("query")
|
|
363
|
+
sp.add_argument("--model", help="limit to one model")
|
|
364
|
+
sp.set_defaults(func=cmd_search)
|
|
365
|
+
|
|
366
|
+
sp = sub.add_parser("path", help="print the on-disk path (for editing files)")
|
|
367
|
+
sp.add_argument("model", nargs="?")
|
|
368
|
+
sp.set_defaults(func=cmd_path)
|
|
369
|
+
|
|
370
|
+
sp = sub.add_parser("ask", help="ask a question answered from the docs via a local model")
|
|
371
|
+
sp.add_argument("model")
|
|
372
|
+
sp.add_argument("question")
|
|
373
|
+
sp.add_argument("--ollama-model", default="llama3", help="ollama model to use as fallback")
|
|
374
|
+
sp.add_argument("--max-context", type=int, default=24000, help="max chars of docs to feed")
|
|
375
|
+
sp.set_defaults(func=cmd_ask)
|
|
376
|
+
|
|
377
|
+
sp = sub.add_parser("update", help="check a model's sources (help, docs, model) for changes")
|
|
378
|
+
sp.add_argument("model", nargs="?", help="model name (omit and use --all for every model)")
|
|
379
|
+
sp.add_argument("--all", action="store_true", help="check every model")
|
|
380
|
+
sp.add_argument("--write", action="store_true", help="accept current state as the new baseline")
|
|
381
|
+
sp.add_argument("--no-docs", action="store_true", help="skip fetching official docs (offline / faster)")
|
|
382
|
+
sp.add_argument("--no-model", action="store_true", help="skip asking the model itself (faster)")
|
|
383
|
+
sp.set_defaults(func=cmd_update)
|
|
384
|
+
|
|
385
|
+
# hooks (Level 1 awareness + Level 2 tailored hooks) — see hooks.py
|
|
386
|
+
from . import hooks as H
|
|
387
|
+
|
|
388
|
+
hp = sub.add_parser("hooks", help="integrate the wiki into a model (awareness + real hooks)")
|
|
389
|
+
hsub = hp.add_subparsers(dest="hooks_cmd", required=True)
|
|
390
|
+
|
|
391
|
+
s = hsub.add_parser("status", help="show integration status per model")
|
|
392
|
+
s.add_argument("model", nargs="?")
|
|
393
|
+
s.add_argument("--all", action="store_true")
|
|
394
|
+
s.set_defaults(func=H.cmd_status)
|
|
395
|
+
|
|
396
|
+
s = hsub.add_parser("enable", help="Level 1: tell a model the wiki exists (dry-run unless --write)")
|
|
397
|
+
s.add_argument("model")
|
|
398
|
+
s.add_argument("--file", help="instructions file to write (default: per-model convention)")
|
|
399
|
+
s.add_argument("--write", action="store_true", help="actually write the change")
|
|
400
|
+
s.set_defaults(func=H.cmd_enable)
|
|
401
|
+
|
|
402
|
+
s = hsub.add_parser("disable", help="Level 1: remove the wiki awareness block")
|
|
403
|
+
s.add_argument("model")
|
|
404
|
+
s.add_argument("--file")
|
|
405
|
+
s.add_argument("--write", action="store_true")
|
|
406
|
+
s.set_defaults(func=H.cmd_disable)
|
|
407
|
+
|
|
408
|
+
s = hsub.add_parser("manifest", help="Level 2: generate the hook-positions doc from the wiki")
|
|
409
|
+
s.add_argument("model")
|
|
410
|
+
s.set_defaults(func=H.cmd_manifest)
|
|
411
|
+
|
|
412
|
+
s = hsub.add_parser("apply", help="Level 2: install the edited hooks (dry-run unless --write)")
|
|
413
|
+
s.add_argument("model")
|
|
414
|
+
s.add_argument("--file", help="target settings file (default: per-model)")
|
|
415
|
+
s.add_argument("--write", action="store_true", help="actually install the hooks")
|
|
416
|
+
s.set_defaults(func=H.cmd_apply)
|
|
417
|
+
|
|
418
|
+
# schedule — config-driven auto-update timer (see schedule.py)
|
|
419
|
+
from . import schedule as S
|
|
420
|
+
|
|
421
|
+
sc = sub.add_parser("schedule", help="auto-update on a timer, configured via a file")
|
|
422
|
+
scsub = sc.add_subparsers(dest="schedule_cmd", required=True)
|
|
423
|
+
|
|
424
|
+
c = scsub.add_parser("config", help="create/show the schedule config file (pick interval here)")
|
|
425
|
+
c.add_argument("--write", action="store_true", help="create the config file")
|
|
426
|
+
c.set_defaults(func=S.cmd_config)
|
|
427
|
+
|
|
428
|
+
c = scsub.add_parser("apply", help="make the timer match the config (dry-run unless --write)")
|
|
429
|
+
c.add_argument("--write", action="store_true", help="actually install/remove the timer")
|
|
430
|
+
c.set_defaults(func=S.cmd_apply)
|
|
431
|
+
|
|
432
|
+
c = scsub.add_parser("status", help="show config + installed timer")
|
|
433
|
+
c.set_defaults(func=S.cmd_status)
|
|
434
|
+
|
|
435
|
+
c = scsub.add_parser("remove", help="remove the scheduled timer (dry-run unless --write)")
|
|
436
|
+
c.add_argument("--write", action="store_true", help="actually remove")
|
|
437
|
+
c.set_defaults(func=S.cmd_remove)
|
|
438
|
+
|
|
439
|
+
return p
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def main(argv=None):
|
|
443
|
+
# Behave like a normal Unix tool when output is piped into `head`/`less`
|
|
444
|
+
# and the reader closes early: die quietly instead of dumping a traceback.
|
|
445
|
+
try:
|
|
446
|
+
import signal
|
|
447
|
+
|
|
448
|
+
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
|
449
|
+
except (ImportError, AttributeError):
|
|
450
|
+
pass # SIGPIPE not available (e.g. Windows)
|
|
451
|
+
args = build_parser().parse_args(argv)
|
|
452
|
+
args.func(args)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
if __name__ == "__main__":
|
|
456
|
+
main()
|