ThinkWiki
LLM wiki kit
First committed by wzdavid
Published in wzdavid/ThinkWiki. Publisher page
38 GitHub stars
What it is
Install once, talk to your agent. ThinkWiki turns documents and notes into a local Markdown wiki with inbox review, content knowledge graph, and browsable HTML workspace.
AutoGineer overview, from the brain’s own files
ThinkWiki is an agent-native skill designed to turn scattered information into a structured, durable local workspace. By integrating directly with skills-compatible AI agents, it allows users to manage their knowledge base through natural conversation rather than manual CLI commands. It supports a wide range of input formats, including PDFs, web pages, and various document types, organizing them into a system that features an inbox for review, a local viewer, and an interactive content knowledge graph. The system is built to be local-first, ensuring that Markdown files remain the source of truth, while providing HTML-based interfaces for visualizing relationships between topics, entities, and claims.
How it works
Memory model: LLM wiki
AutoGineer overview, from the brain’s own files
ThinkWiki functions as a skill installed into an AI agent's environment. It uses a set of Python scripts to manage a local directory of Markdown files. The system processes inputs through an inbox workflow, where content is reviewed before being ingested into the wiki. It generates a series of HTML outputs, including a workspace home, an inbox console, a page viewer, and a graph explorer. Users interact with these features by asking their agent to perform tasks like clipping content, building the knowledge graph, or serving the workspace via a local loopback server.
| Folder | Role | Files |
|---|---|---|
| .github | agent-config | 1 |
| docs | docs | 41 |
| scripts | scripts | 34 |
| templates | templates | 13 |
| tests | tests | 1 |
The agent reads
- SKILL.md evidence: README.md
Requirements
Runtimes: Python
API keys (by name): THINKWIKI_EMBED_API_KEY, THINKWIKI_LLM_API_KEY
Set it up
Clone the repository at the commit AutoGineer scanned:
git clone https://github.com/wzdavid/ThinkWiki.git && cd ThinkWiki && git checkout 89f477d7a4095ba2ecf49ef9332e240c6f1208a1cd ThinkWiki && claude
AutoGineer scanned these files at commit 89f477d.
- Not scanned by AutoGineerThe author’s README also describes installing it from a clone of its repository. Read its install section: AutoGineer did not scan that path. https://github.com/wzdavid/ThinkWiki/blob/89f477d7a4095ba2ecf49ef9332e240c6f1208a1/README.md#manual-install-optional
First committed by wzdavid. Published in wzdavid/ThinkWiki. Licence: MIT License.
Usage
AutoGineer overview, from the brain’s own files
- Initialize a new local wiki workspace
- Clip web pages, files, or text into an inbox for later processing
- Ingest reviewed items into the permanent wiki structure
- Query the wiki for information with evidence-based responses
- Generate and visualize a content knowledge graph
- Serve the wiki workspace as a local HTML site for browser-based navigation
- “Create a local knowledge base named Research Notes in my workspace.”
- The agent initializes a new ThinkWiki workspace structure in the specified directory.
- “Clip this article into the inbox: https://example.com/article”
- The agent saves the content of the URL into the ThinkWiki inbox for later review.
- “What did we already decide about context budgets?”
- The agent searches the existing wiki pages and provides an answer supported by evidence from the knowledge base.
- “Build the knowledge graph and open the workspace home in my browser.”
- The agent generates the graph visualization and starts the local server to display the workspace in the browser.
Documentation by wzdavid
ThinkWiki
Image: Release Image: License Image: Python
ThinkWiki is an agent-native local knowledge base skill. Talk to your agent to turn scattered documents, web pages, notes, and conversations into a durable Markdown workspace — with inbox review, local browsing, and an interactive content knowledge graph you can open in a browser.
Chinese version: README.zh.md
Repository: https://github.com/wzdavid/ThinkWiki
Works With Skills-Compatible Agents
ThinkWiki follows the open Agent Skills format (SKILL.md + bundled scripts). If your agent can install skills and run local commands, it can run ThinkWiki.
You do not need to memorize CLI commands. Install the skill once, then create and manage your wiki entirely through conversation.
| Agent / Host | Typical use |
|---|---|
| Claude Code | Install the repo as a skill, then ask it to build and query your wiki |
| OpenClaw | Full skill workflow; use serve + the built-in browser to open HTML outputs |
| Trae | Install under .trae/skills, then manage the wiki in chat |
| Hermes Agent | Local skill directory + shell execution |
| OpenAI Codex / Codex CLI | Skills-compatible coding agents with local file access |
| Cursor | Agent mode with project skills |
| Gemini CLI | Skills-compatible CLI agent |
| GitHub Copilot | Agent / coding workflows in VS Code that support skills |
| Other skills hosts | Any product that loads SKILL.md and can run python3 scripts/thinkwiki ... locally |
Requirement: a local environment with Python 3. ThinkWiki bootstraps its own runtime on first use. Pure cloud chat without local file or shell access cannot run a local wiki.
Get Started: Install Through Conversation
Point your agent at this repository and ask it to install the skill for you.
Example prompt:
Please install the ThinkWiki skill from https://github.com/wzdavid/ThinkWiki , bootstrap the runtime, and run a health check to confirm everything works on my machine.
The agent will typically clone or copy the skill into your host's skills directory, run bootstrap, and verify capabilities with doctor. You should not need to run those steps yourself unless you prefer manual setup.
After installation, start using it immediately:
Create a local knowledge base named
My Wikiin my workspace.
Clip this article into the inbox first:
https://example.com/article
Import this PDF into the wiki:
/path/to/file.pdf
Answer from my wiki: what did we already decide about context budgets?
Build the knowledge graph and open the workspace home in my browser.
The agent reads SKILL.md, maps your intent to stable ThinkWiki actions, and reports what changed — including paths to Markdown pages and HTML outputs.
Why ThinkWiki
Agent-first: users talk to an agent, not to a long list of commands.
Local-first: Markdown files remain the source of truth on your machine.
HTML-first: inbox, viewer, graph, and governance pages are real browsable workspaces.
Knowledge-first: the graph is a content knowledge graph — topics, concepts, entities, claims, and governance — not just a file-link map.
What It Produces
ThinkWiki generates a workspace that includes:
output/index.html: unified workspace homeoutput/inbox/index.html: inbox review consoleoutput/viewer/index.html: local page browseroutput/graph/index.html: interactive graph exploreroutput/graph/report.html: graph governance reportoutput/graph/entity-merge-review.html: entity merge review pageoutput/graph/entity-merge-plan.html: entity merge dry-run preview when requested
Preview
Workspace Home (output/index.html)
The unified workspace home summarizes recent changes, recommended next actions, inbox backlog, and a graph snapshot so you can decide whether to read, ingest, or explore.
Image: ThinkWiki workspace home preview
Inbox Review (output/inbox/index.html)
Review clipped web pages, files, and notes before formal ingest. Items are grouped into ready, review, and weak so you can process the highest-value captures first.
Image: ThinkWiki inbox review preview
Local Viewer (output/viewer/index.html)
Browse the entire wiki by page type, confidence, and status. Open any page directly in the viewer without leaving the local HTML workspace.
Image: ThinkWiki local viewer preview
Knowledge Graph (output/graph/index.html)
Explore the content knowledge graph interactively. Switch between knowledge, document, and suggested views to inspect semantic relations and candidate edges.
Image: ThinkWiki knowledge graph preview
Graph Governance Report (output/graph/report.html)
Inspect graph health signals such as isolated pages, hub stubs, fragile bridges, suggested links, and entity merge candidates before making structural changes.
Image: ThinkWiki graph governance report preview
Everyday Commands (In Plain Language)
Once ThinkWiki is installed, these are the kinds of requests users make most often:
| You say | ThinkWiki does |
|---|---|
Create a wiki called Research Notes | Initializes a local workspace |
| Clip / save this URL or file to inbox | Captures content for later review |
| Ingest the ready inbox items | Promotes reviewed captures into wiki pages |
| What does my wiki say about X? | Evidence-first Q&A from existing pages |
| Save this answer as a query page | Persists high-value outputs |
| Show me the knowledge graph | Builds/refreshes graph HTML |
| Review entity merge candidates | Surfaces alias collisions for manual confirmation |
| Open the wiki workspace in my browser | Runs serve and returns http://127.0.0.1:8765/index.html |
Core Capabilities
Initialize a local wiki workspace
Clip web pages, files, or pasted text into an inbox for later review
Import Markdown, PDF, DOCX, XLSX, XLS, PPTX, web pages, and plain text
Answer questions from existing wiki pages with evidence-first behavior
Save high-value outputs as
query,synthesis,decision, orconceptpagesBuild a content knowledge graph with
knowledge,document, andsuggestedviewsGenerate entity governance outputs, including ambiguous alias review and deterministic merge flows
Check runtime health, workspace status, and graph governance state
Content Knowledge Graph
ThinkWiki v1.6.0 builds a schema v2 graph with default_view = knowledge.
The knowledge view can contain:
page-backed nodes such as
source,topic,concept,decision,synthesis,query, andentityclaim nodes extracted from structured wiki content
semantic relations such as
about,belongs_to,depends_on,asserts,supports,contradicts, andsuggests_related_to
This means the graph is not limited to raw file references. It can represent knowledge structure, evidence structure, and entity governance directly.
Browse HTML Outputs
Agent chat UIs usually cannot render ThinkWiki HTML inline. Ask your agent to start the built-in loopback server when you want to inspect inbox, viewer, graph, or governance pages:
Start the ThinkWiki output server and give me the workspace URL.
Or run it yourself:
python3 scripts/thinkwiki serve --root /path/to/my-wikiBy default this serves <wiki-root>/output/ at http://127.0.0.1:8765/. Start from http://127.0.0.1:8765/index.html.
On OpenClaw, after serve is running, the agent can also open the workspace home with:
openclaw browser --browser-profile openclaw open http://127.0.0.1:8765/index.htmlOn other agents, open the same URL in your system browser, or use whatever browser tool the host provides.
Use serve --print-urls when you only need the URL list without starting the server.
Environment Variables
Remote AI features are disabled by default. Configure only what you need:
Content generation (crystallize, digest)
Requires all three variables. Any OpenAI-compatible chat completion endpoint works (MiniMax, OpenAI, DeepSeek, local Ollama, etc.).
| Variable | Required | Purpose |
|---|---|---|
THINKWIKI_LLM_API_KEY | Yes, to enable | API key for the configured LLM provider |
THINKWIKI_LLM_BASE_URL | Yes, to enable | Chat completions URL, e.g. https://api.openai.com/v1/chat/completions |
THINKWIKI_LLM_MODEL | Yes, to enable | Model name, e.g. gpt-4o-mini or MiniMax-M3 |
THINKWIKI_LLM_TEMPERATURE | No | Optional temperature override |
Without a complete LLM configuration, crystallize and digest use local heuristics only.
Entity embedding (entity-merge-review, graph-report, health)
Enabled when THINKWIKI_EMBED_API_KEY is set. Defaults to SiliconFlow BGE-M3; override the endpoint or model if needed.
| Variable | Required | Purpose |
|---|---|---|
THINKWIKI_EMBED_API_KEY | Yes, to enable | API key for the embedding provider |
THINKWIKI_EMBED_BASE_URL | No | Embedding URL(s), comma-separated; default https://api.siliconflow.cn/v1/embeddings |
THINKWIKI_EMBED_MODEL | No | Embedding model; default BAAI/bge-m3 |
Register a free SiliconFlow key at https://siliconflow.cn if you want the default embedding setup.
Manual Install (Optional)
If you prefer to set up the skill yourself instead of asking an agent:
git clone https://github.com/wzdavid/ThinkWiki ThinkWiki
cd ThinkWiki
python3 scripts/thinkwiki bootstrap
python3 scripts/thinkwiki doctor --repo-root .Then install the folder into your agent's skills directory according to that host's documentation.
CLI Reference (For Agents And Power Users)
ThinkWiki keeps one stable entry point that agents call under the hood:
python3 scripts/thinkwiki <command> [args]Common commands: init, clip, ingest, inbox, viewer, graph, graph-report, entity-merge-review, entity-merge-apply, serve, health, status, doctor.
See SKILL.md for the agent behavior contract and intent mapping.
Repository Docs
README.md: concise English project overviewREADME.zh.md: Chinese overviewSKILL.md: skill behavior contract for agent hostsCHANGELOG.md: release history
License
MIT
scripts/graph_report.py
Shown as plain text.
#!/usr/bin/env python3
from __future__ import annotations
"""
ThinkWiki Script: graph_report
Purpose:
- Generate a governance report from the current graph outputs.
Usage:
- Prefer `python scripts/thinkwiki graph-report ...`.
- Run `python scripts/<script> --help` for direct CLI details when the file exposes its own arguments.
"""
import argparse
import json
from html import escape
from pathlib import Path
from build_graph import node_metrics
from utils import ambiguous_entity_merge_candidates, append_log, file_uri, find_repo_root, print_output_serve_hint, today_str, write_output_home, write_text
def _load_graph(root: Path) -> dict[str, object]:
graph_path = root / "output" / "graph" / "graph.json"
if not graph_path.exists():
raise SystemExit("Graph data not found. Run `python scripts/thinkwiki graph --root <wiki-root>` first.")
try:
return json.loads(graph_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError) as exc:
raise SystemExit(f"Invalid graph data: {graph_path} ({exc})") from exc
def _graph_view(graph: dict[str, object], view_name: str = "knowledge") -> dict[str, object]:
views = graph.get("views", {})
if isinstance(views, dict):
view = views.get(view_name, {})
if isinstance(view, dict):
nodes = view.get("nodes", [])
edges = view.get("edges", [])
insights = view.get("insights", {})
if isinstance(nodes, list) and isinstance(edges, list) and isinstance(insights, dict):
return {"nodes": nodes, "edges": edges, "insights": insights, "name": view_name}
return {
"nodes": graph.get("nodes", []) if isinstance(graph.get("nodes"), list) else [],
"edges": graph.get("edges", []) if isinstance(graph.get("edges"), list) else [],
"insights": graph.get("insights", {}) if isinstance(graph.get("insights"), dict) else {},
"name": "legacy",
}
def _page_nodes(nodes: list[dict[str, object]]) -> list[dict[str, object]]:
return [node for node in nodes if str(node.get("type") or "page") not in {"raw", "file", "claim"}]
def _entity_nodes(nodes: list[dict[str, object]]) -> list[dict[str, object]]:
return [node for node in nodes if str(node.get("type") or "page") == "entity"]
def _entity_alias_stats(entity_nodes: list[dict[str, object]]) -> tuple[int, int]:
alias_count = 0
aliased_entity_count = 0
for node in entity_nodes:
aliases = node.get("aliases", [])
if not isinstance(aliases, list):
continue
clean_aliases = [str(item).strip() for item in aliases if str(item).strip()]
alias_count += len(clean_aliases)
if clean_aliases:
aliased_entity_count += 1
return alias_count, aliased_entity_count
def _summary_length(node: dict[str, object]) -> int:
return len(str(node.get("summary", "") or "").strip())
def _connected_components(node_ids: set[str], edges: list[dict[str, str]]) -> list[list[str]]:
adjacency: dict[str, set[str]] = {node_id: set() for node_id in node_ids}
for edge in edges:
source = str(edge.get("source") or "")
target = str(edge.get("target") or "")
if source not in adjacency or target not in adjacency:
continue
adjacency[source].add(target)
adjacency[target].add(source)
components: list[list[str]] = []
seen: set[str] = set()
for node_id in sorted(node_ids):
if node_id in seen:
continue
stack = [node_id]
component: list[str] = []
while stack:
current = stack.pop()
if current in seen:
continue
seen.add(current)
component.append(current)
stack.extend(sorted(adjacency[current] - seen))
components.append(sorted(component))
return sorted(components, key=lambda item: (-len(item), item[0] if item else ""))
def _hub_stub_candidates(
page_nodes: list[dict[str, object]],
metrics: dict[str, dict[str, object]],
) -> list[dict[str, object]]:
if not page_nodes:
return []
degrees = [int(metrics[str(node["id"])]["degree"]) for node in page_nodes]
average_degree = sum(degrees) / len(degrees)
threshold = max(3, int(round(average_degree + 1)))
items: list[dict[str, object]] = []
for node in page_nodes:
node_id = str(node["id"])
if str(node.get("type") or "page") == "entity":
continue
degree = int(metrics[node_id]["degree"])
summary_length = _summary_length(node)
if degree < threshold or summary_length >= 80:
continue
items.append({
"id": node_id,
"title": str(node.get("label") or node_id),
"type": str(node.get("type") or "page"),
"degree": degree,
"summaryLength": summary_length,
"reason": f"Degree is {degree}, but the summary is only {summary_length} characters long.",
})
return sorted(items, key=lambda item: (-int(item["degree"]), int(item["summaryLength"]), str(item["title"]).lower()))[:6]
def _fragile_bridge_candidates(
page_nodes: list[dict[str, object]],
metrics: dict[str, dict[str, object]],
) -> list[dict[str, object]]:
items: list[dict[str, object]] = []
for node in page_nodes:
node_id = str(node["id"])
info = metrics[node_id]
degree = int(info["degree"])
neighbor_types = {str(item) for item in info["neighbor_types"]}
if degree > 0 and degree <= 2 and len(neighbor_types) >= 2:
items.append({
"id": node_id,
"title": str(node.get("label") or node_id),
"type": str(node.get("type") or "page"),
"degree": degree,
"neighborTypes": sorted(neighbor_types),
"reason": f"Only {degree} relations connect {len(neighbor_types)} page types, so this bridge is fragile.",
})
return sorted(items, key=lambda item: (int(item["degree"]), -len(item["neighborTypes"]), str(item["title"]).lower()))[:6]
def _page_health_candidates(
page_nodes: list[dict[str, object]],
metrics: dict[str, dict[str, object]],
) -> list[dict[str, object]]:
items: list[dict[str, object]] = []
for node in page_nodes:
node_id = str(node["id"])
degree = int(metrics[node_id]["degree"])
if degree == 0:
items.append({
"id": node_id,
"title": str(node.get("label") or node_id),
"type": str(node.get("type") or "page"),
"severity": "isolated",
"degree": degree,
"reason": "No page-level relationships yet.",
})
elif degree == 1:
items.append({
"id": node_id,
"title": str(node.get("label") or node_id),
"type": str(node.get("type") or "page"),
"severity": "weak",
"degree": degree,
"reason": "Only one page-level relationship so far. Add more links.",
})
return sorted(
items,
key=lambda item: (
0 if str(item["severity"]) == "isolated" else 1,
str(item["title"]).lower(),
),
)[:8]
def _isolated_cluster_candidates(
page_nodes: list[dict[str, object]],
edges: list[dict[str, str]],
node_by_id: dict[str, dict[str, object]],
) -> list[dict[str, object]]:
page_node_ids = {str(node["id"]) for node in page_nodes}
components = _connected_components(page_node_ids, edges)
if len(components) <= 1:
return []
items: list[dict[str, object]] = []
for component in components[1:]:
if len(component) < 2:
continue
labels = [str(node_by_id[node_id].get("label") or node_id) for node_id in component if node_id in node_by_id]
items.append({
"size": len(component),
"nodeIds": component,
"titles": labels[:4],
"reason": f"This cluster is disconnected from the main graph and contains {len(component)} pages.",
})
return sorted(items, key=lambda item: (-int(item["size"]), ",".join(item["titles"]).lower()))[:4]
def _top_actions(report: dict[str, object]) -> list[str]:
stats = report["stats"]
assert isinstance(stats, dict)
actions: list[str] = []
if int(stats.get("isolatedPageCount", 0) or 0):
actions.append("Fix isolated pages first by adding `links_to` edges or source references.")
if int(stats.get("hubStubCount", 0) or 0):
actions.append("Strengthen high-degree thin pages with better summaries, context, and sources.")
if int(stats.get("fragileBridgeCount", 0) or 0):
actions.append("Reinforce fragile bridge pages so cross-topic links do not rely on single points.")
if int(stats.get("isolatedClusterCount", 0) or 0):
actions.append("Reconnect isolated clusters back to the main graph.")
if int(stats.get("suggestedLinkCount", 0) or 0):
actions.append("Review suggested links and convert strong candidates into explicit page links.")
if int(stats.get("ambiguousAliasGroupCount", 0) or 0):
actions.append("Review ambiguous alias groups so multiple entity pages do not collapse into the same identity key unexpectedly.")
return actions[:4] or ["The graph structure looks stable. Continue expanding high-value pages."]
def _console_summary(report: dict[str, object]) -> str:
stats = report["stats"]
assert isinstance(stats, dict)
return (
"pages={pages}, relations={relations}, entities={entities}, aliasedEntities={aliased_entities}, aliases={aliases}, ambiguousAliasGroups={ambiguous_groups}, ambiguousEntities={ambiguous_entities}, "
"isolatedPages={isolated}, weakPages={weak}, hubStubs={hub_stubs}, fragileBridges={fragile}, isolatedClusters={clusters}, suggestedLinks={suggested}"
).format(
pages=int(stats.get("nodeCount", 0) or 0),
relations=int(stats.get("edgeCount", 0) or 0),
entities=int(stats.get("entityCount", 0) or 0),
aliased_entities=int(stats.get("aliasedEntityCount", 0) or 0),
aliases=int(stats.get("aliasCount", 0) or 0),
ambiguous_groups=int(stats.get("ambiguousAliasGroupCount", 0) or 0),
ambiguous_entities=int(stats.get("ambiguousEntityCount", 0) or 0),
isolated=int(stats.get("isolatedPageCount", 0) or 0),
weak=int(stats.get("weakPageCount", 0) or 0),
hub_stubs=int(stats.get("hubStubCount", 0) or 0),
fragile=int(stats.get("fragileBridgeCount", 0) or 0),
clusters=int(stats.get("isolatedClusterCount", 0) or 0),
suggested=int(stats.get("suggestedLinkCount", 0) or 0),
)
def _console_actions(report: dict[str, object]) -> list[str]:
stats = report["stats"]
assert isinstance(stats, dict)
actions: list[str] = []
if int(stats.get("isolatedPageCount", 0) or 0):
actions.append("Fix isolated pages first by adding links or source references.")
if int(stats.get("hubStubCount", 0) or 0):
actions.append("Strengthen high-degree thin pages with better summaries and context.")
if int(stats.get("fragileBridgeCount", 0) or 0):
actions.append("Reinforce fragile bridge pages so cross-topic links do not rely on single points.")
if int(stats.get("isolatedClusterCount", 0) or 0):
actions.append("Reconnect isolated clusters back to the main graph.")
if int(stats.get("suggestedLinkCount", 0) or 0):
actions.append("Review suggested links and convert strong candidates into explicit page links.")
if int(stats.get("ambiguousAliasGroupCount", 0) or 0):
actions.append("Review ambiguous entity merge candidates before changing canonical entity pages.")
return actions[:4] or ["Graph structure looks stable. Continue expanding high-value pages."]
def build_report(graph: dict[str, object]) -> dict[str, object]:
active_view = _graph_view(graph, "knowledge")
nodes = active_view.get("nodes", [])
edges = active_view.get("edges", [])
insights = active_view.get("insights", {})
if not isinstance(nodes, list) or not isinstance(edges, list):
raise SystemExit("Invalid graph payload: nodes/edges are missing.")
if not isinstance(insights, dict):
insights = {}
page_nodes = _page_nodes(nodes)
entity_nodes = _entity_nodes(nodes)
page_node_ids = {str(node["id"]) for node in page_nodes}
filtered_edges = [
edge for edge in edges
if str(edge.get("source") or "") in page_node_ids and str(edge.get("target") or "") in page_node_ids
]
metrics = node_metrics(page_nodes, filtered_edges)
node_by_id = {str(node["id"]): node for node in page_nodes}
isolated_pages = _page_health_candidates(page_nodes, metrics)
hub_stubs = _hub_stub_candidates(page_nodes, metrics)
fragile_bridges = _fragile_bridge_candidates(page_nodes, metrics)
isolated_clusters = _isolated_cluster_candidates(page_nodes, filtered_edges, node_by_id)
suggested_links = [
item for item in insights.get("suggestedLinks", [])
if isinstance(item, dict)
]
isolated_entities = sorted(
[
{
"id": str(node["id"]),
"title": str(node.get("label") or node["id"]),
"type": "entity",
"reason": "This entity node does not connect to any other knowledge nodes yet.",
}
for node in entity_nodes
if not any(
str(edge.get("source") or "") == str(node["id"]) or str(edge.get("target") or "") == str(node["id"])
for edge in edges
)
],
key=lambda item: str(item["title"]).lower(),
)[:8]
alias_count, aliased_entity_count = _entity_alias_stats(entity_nodes)
ambiguous_merge_candidates, ambiguous_entity_count = ambiguous_entity_merge_candidates(entity_nodes)
stats = {
"view": str(active_view.get("name") or "knowledge"),
"nodeCount": len(page_nodes),
"edgeCount": len(filtered_edges),
"entityCount": len(entity_nodes),
"aliasCount": alias_count,
"aliasedEntityCount": aliased_entity_count,
"ambiguousAliasGroupCount": len(ambiguous_merge_candidates),
"ambiguousEntityCount": ambiguous_entity_count,
"isolatedEntityCount": len(isolated_entities),
"isolatedPageCount": sum(1 for item in isolated_pages if str(item.get("severity", "")) == "isolated"),
"weakPageCount": sum(1 for item in isolated_pages if str(item.get("severity", "")) == "weak"),
"bridgePageCount": len([item for item in insights.get("bridgeNodes", []) if isinstance(item, dict)]),
"suggestedLinkCount": len(suggested_links),
"hubStubCount": len(hub_stubs),
"fragileBridgeCount": len(fragile_bridges),
"isolatedClusterCount": len(isolated_clusters),
"componentCount": len(_connected_components(page_node_ids, filtered_edges)) if page_node_ids else 0,
"averageDegree": round((len(filtered_edges) * 2 / len(page_nodes)) if page_nodes else 0, 2),
}
top_node = next((item for item in insights.get("topNodes", []) if isinstance(item, dict)), None)
summary_parts = []
if top_node:
summary_parts.append(f"The current key page is {top_node.get('title', 'n/a')}.")
if stats["isolatedPageCount"]:
summary_parts.append(f"There are {stats['isolatedPageCount']} isolated pages that need links.")
elif stats["weakPageCount"]:
summary_parts.append(f"There are {stats['weakPageCount']} weakly connected pages that need more context.")
else:
summary_parts.append("The main pages already have a basic connection structure.")
if stats["hubStubCount"]:
summary_parts.append(f"There are {stats['hubStubCount']} high-degree thin pages worth strengthening first.")
if stats["isolatedClusterCount"]:
summary_parts.append(f"There are {stats['isolatedClusterCount']} clusters disconnected from the main graph.")
if stats["suggestedLinkCount"]:
summary_parts.append(f"There are {stats['suggestedLinkCount']} suggested links ready for review.")
if stats["entityCount"]:
summary_parts.append(f"The graph currently identifies {stats['entityCount']} entity nodes.")
if stats["aliasCount"]:
summary_parts.append(f"Among them, {stats['aliasedEntityCount']} entities carry aliases for a total of {stats['aliasCount']} alias entries.")
if stats["ambiguousAliasGroupCount"]:
summary_parts.append(
f"There are also {stats['ambiguousAliasGroupCount']} ambiguous alias groups spanning {stats['ambiguousEntityCount']} entity pages. Review them manually."
)
report = {
"generated_at": today_str(),
"schemaVersion": str(graph.get("schema_version") or "1"),
"view": str(active_view.get("name") or "knowledge"),
"summary": " ".join(summary_parts),
"stats": stats,
"topActions": _top_actions({"stats": stats}),
"topNodes": insights.get("topNodes", []),
"bridgeNodes": insights.get("bridgeNodes", []),
"isolatedPages": isolated_pages,
"isolatedEntities": isolated_entities,
"ambiguousEntityMergeCandidates": ambiguous_merge_candidates,
"hubStubs": hub_stubs,
"fragileBridges": fragile_bridges,
"isolatedClusters": isolated_clusters,
"suggestedLinks": suggested_links,
}
return report
def render_report_markdown(report: dict[str, object]) -> str:
stats = report["stats"]
assert isinstance(stats, dict)
lines = [
"# ThinkWiki Graph Report",
"",
f"- Generated: {report['generated_at']}",
f"- Summary: {report['summary']}",
"",
"## Health Summary",
"",
f"- Pages: {stats['nodeCount']}",
f"- Relations: {stats['edgeCount']}",
f"- Entities: {stats['entityCount']}",
f"- Aliased Entities: {stats['aliasedEntityCount']}",
f"- Aliases: {stats['aliasCount']}",
f"- Ambiguous Alias Groups: {stats['ambiguousAliasGroupCount']}",
f"- Ambiguous Entities: {stats['ambiguousEntityCount']}",
f"- Isolated Entities: {stats['isolatedEntityCount']}",
f"- Average Degree: {stats['averageDegree']}",
f"- Isolated Pages: {stats['isolatedPageCount']}",
f"- Weak Pages: {stats['weakPageCount']}",
f"- Bridge Pages: {stats['bridgePageCount']}",
f"- Suggested Links: {stats['suggestedLinkCount']}",
f"- Hub Stubs: {stats['hubStubCount']}",
f"- Fragile Bridges: {stats['fragileBridgeCount']}",
f"- Isolated Clusters: {stats['isolatedClusterCount']}",
"",
"## Top Actions",
"",
]
lines.extend(f"- {item}" for item in report["topActions"])
lines.extend(["", "## Entities That Need Links", ""])
if report["isolatedEntities"]:
lines.extend(
f"- {item['title']} | {item['reason']}"
for item in report["isolatedEntities"]
)
else:
lines.append("- No isolated entities")
lines.extend(["", "## Ambiguous Entity Merge Candidates", ""])
if report["ambiguousEntityMergeCandidates"]:
lines.extend(
f"- key={item['identityKey']} | {', '.join(item['titles'])} | {item['reason']}"
for item in report["ambiguousEntityMergeCandidates"]
)
else:
lines.append("- No ambiguous merge candidates")
lines.extend(["", "## Hub Stubs", ""])
if report["hubStubs"]:
lines.extend(
f"- {item['title']} ({item['type']}) | degree={item['degree']} | {item['reason']}"
for item in report["hubStubs"]
)
else:
lines.append("- No hub stubs")
lines.extend(["", "## Fragile Bridges", ""])
if report["fragileBridges"]:
lines.extend(
f"- {item['title']} ({item['type']}) | degree={item['degree']} | {item['reason']}"
for item in report["fragileBridges"]
)
else:
lines.append("- No fragile bridges")
lines.extend(["", "## Isolated Clusters", ""])
if report["isolatedClusters"]:
lines.extend(
f"- size={item['size']} | {', '.join(item['titles'])} | {item['reason']}"
for item in report["isolatedClusters"]
)
else:
lines.append("- No isolated clusters")
lines.extend(["", "## Suggested Links", ""])
if report["suggestedLinks"]:
lines.extend(
f"- {item['source']} <-> {item['target']} | score={item['score']}"
for item in report["suggestedLinks"]
)
else:
lines.append("- No suggested links")
return "\n".join(lines)
def _render_html_list(items: list[str]) -> str:
if not items:
return "<div class='empty'>Nothing here yet.</div>"
return "<ul class='bullet-list'>{}</ul>".format(
"".join(f"<li>{escape(item)}</li>" for item in items)
)
def _render_html_metric_cards(stats: dict[str, object]) -> str:
cards = []
for label, key in [
("Pages", "nodeCount"),
("Relations", "edgeCount"),
("Entities", "entityCount"),
("Aliased Entities", "aliasedEntityCount"),
("Aliases", "aliasCount"),
("Ambiguous Alias Groups", "ambiguousAliasGroupCount"),
("Ambiguous Entities", "ambiguousEntityCount"),
("Isolated Entities", "isolatedEntityCount"),
("Average Degree", "averageDegree"),
("Isolated Pages", "isolatedPageCount"),
("Weak Pages", "weakPageCount"),
("Bridge Pages", "bridgePageCount"),
("Suggested Links", "suggestedLinkCount"),
("Hub Stubs", "hubStubCount"),
("Fragile Bridges", "fragileBridgeCount"),
("Isolated Clusters", "isolatedClusterCount"),
]:
cards.append(
"<div class='stat'><strong>{}</strong><span>{}</span></div>".format(
escape(str(stats.get(key, 0) or 0)),
escape(label),
)
)
return "\n".join(cards)
def _render_html_record_cards(items: list[dict[str, object]], empty_text: str, formatter) -> str:
if not items:
return "<div class='empty'>{}</div>".format(escape(empty_text))
rows = []
for item in items:
title, meta, reason = formatter(item)
rows.append(
"<div class='record-card'>"
"<strong>{}</strong>"
"<div class='record-meta'>{}</div>"
"<p>{}</p>"
"</div>".format(
escape(title),
escape(meta),
escape(reason),
)
)
return "\n".join(rows)
def render_report_html(report: dict[str, object]) -> str:
stats = report["stats"]
assert isinstance(stats, dict)
top_actions = report.get("topActions", [])
hub_stubs = report.get("hubStubs", [])
fragile_bridges = report.get("fragileBridges", [])
isolated_clusters = report.get("isolatedClusters", [])
suggested_links = report.get("suggestedLinks", [])
isolated_pages = report.get("isolatedPages", [])
isolated_entities = report.get("isolatedEntities", [])
ambiguous_merge_candidates = report.get("ambiguousEntityMergeCandidates", [])
top_nodes = report.get("topNodes", [])
bridge_nodes = report.get("bridgeNodes", [])
isolated_cards = _render_html_record_cards(
isolated_pages if isinstance(isolated_pages, list) else [],
"No isolated or weak pages.",
lambda item: (
str(item.get("title") or item.get("id") or "Untitled"),
"{} | degree={}".format(str(item.get("severity") or "page"), int(item.get("degree", 0) or 0)),
str(item.get("reason") or ""),
),
)
hub_cards = _render_html_record_cards(
hub_stubs if isinstance(hub_stubs, list) else [],
"No hub stubs.",
lambda item: (
str(item.get("title") or item.get("id") or "Untitled"),
"{} | degree={}".format(str(item.get("type") or "page"), int(item.get("degree", 0) or 0)),
str(item.get("reason") or ""),
),
)
bridge_cards = _render_html_record_cards(
fragile_bridges if isinstance(fragile_bridges, list) else [],
"No fragile bridges.",
lambda item: (
str(item.get("title") or item.get("id") or "Untitled"),
"{} | degree={}".format(str(item.get("type") or "page"), int(item.get("degree", 0) or 0)),
str(item.get("reason") or ""),
),
)
cluster_cards = _render_html_record_cards(
isolated_clusters if isinstance(isolated_clusters, list) else [],
"No isolated clusters.",
lambda item: (
", ".join(str(title) for title in item.get("titles", [])[:3]) or "Untitled cluster",
"cluster | size={}".format(int(item.get("size", 0) or 0)),
str(item.get("reason") or ""),
),
)
suggestion_cards = _render_html_record_cards(
suggested_links if isinstance(suggested_links, list) else [],
"No suggested links.",
lambda item: (
"{} <-> {}".format(str(item.get("source") or "n/a"), str(item.get("target") or "n/a")),
"suggested link | score={}".format(item.get("score", "n/a")),
str(item.get("reason") or "High-confidence structural suggestion."),
),
)
isolated_entity_cards = _render_html_record_cards(
isolated_entities if isinstance(isolated_entities, list) else [],
"No isolated entities.",
lambda item: (
str(item.get("title") or item.get("id") or "Untitled"),
str(item.get("type") or "entity"),
str(item.get("reason") or ""),
),
)
ambiguous_merge_cards = _render_html_record_cards(
ambiguous_merge_candidates if isinstance(ambiguous_merge_candidates, list) else [],
"No ambiguous merge candidates.",
lambda item: (
", ".join(str(title) for title in item.get("titles", [])[:3]) or "Untitled",
"identity key={}".format(str(item.get("identityKey") or "n/a")),
str(item.get("reason") or ""),
),
)
top_node_cards = _render_html_record_cards(
top_nodes if isinstance(top_nodes, list) else [],
"No key pages yet.",
lambda item: (
str(item.get("title") or item.get("id") or "Untitled"),
"{} | score={}".format(str(item.get("type") or "page"), int(item.get("score", 0) or 0)),
str(item.get("reason") or ""),
),
)
bridge_node_cards = _render_html_record_cards(
bridge_nodes if isinstance(bridge_nodes, list) else [],
"No bridge pages yet.",
lambda item: (
str(item.get("title") or item.get("id") or "Untitled"),
"{} | score={}".format(str(item.get("type") or "page"), int(item.get("score", 0) or 0)),
str(item.get("reason") or ""),
),
)
return """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ThinkWiki Graph Report</title>
<style>
:root {{
--bg: #0b1020;
--panel: #121935;
--text: #edf2ff;
--muted: #a8b3cf;
--border: rgba(255,255,255,0.1);
--accent: #8ab4ff;
}}
* {{ box-sizing: border-box; }}
body {{
margin: 0;
min-height: 100vh;
font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: linear-gradient(180deg, #0b1020 0%, #10172f 100%);
color: var(--text);
padding: 24px;
}}
.shell {{
width: min(1180px, 100%);
margin: 0 auto;
background: rgba(9, 13, 28, 0.86);
border: 1px solid var(--border);
border-radius: 24px;
padding: 28px;
}}
h1, h2, p {{ margin-top: 0; }}
.lead {{
color: var(--muted);
line-height: 1.6;
}}
.meta {{
display: flex;
gap: 10px;
flex-wrap: wrap;
margin: 16px 0 24px;
}}
.badge {{
border: 1px solid var(--border);
border-radius: 999px;
padding: 6px 12px;
color: var(--muted);
background: rgba(255,255,255,0.03);
font-size: 0.92rem;
}}
.stats {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
margin: 24px 0;
}}
.stat {{
border: 1px solid var(--border);
background: var(--panel);
border-radius: 18px;
padding: 16px;
}}
.stat strong {{
display: block;
font-size: 1.4rem;
margin-bottom: 6px;
}}
.stat span {{
color: var(--muted);
}}
.layout {{
display: grid;
gap: 14px;
grid-template-columns: repeat(2, minmax(0, 1fr));
margin-top: 18px;
}}
.panel {{
border: 1px solid var(--border);
background: rgba(255,255,255,0.02);
border-radius: 18px;
padding: 18px;
}}
.panel p {{
color: var(--muted);
line-height: 1.55;
}}
.bullet-list {{
margin: 0;
padding-left: 18px;
color: var(--muted);
display: grid;
gap: 10px;
line-height: 1.5;
}}
.records {{
display: grid;
gap: 10px;
}}
.record-card {{
border: 1px solid var(--border);
background: var(--panel);
border-radius: 16px;
padding: 14px;
}}
.record-card strong {{
display: block;
margin-bottom: 6px;
}}
.record-meta {{
color: var(--accent);
font-size: 0.88rem;
margin-bottom: 8px;
}}
.record-card p {{
margin-bottom: 0;
}}
.empty {{
border: 1px dashed var(--border);
border-radius: 16px;
padding: 14px;
color: var(--muted);
}}
a {{
color: var(--accent);
}}
@media (max-width: 860px) {{
.layout {{
grid-template-columns: 1fr;
}}
}}
</style>
</head>
<body>
<main class="shell">
<h1>ThinkWiki Graph Report</h1>
<p class="lead">{summary}</p>
<div class="meta">
<span class="badge">Generated {generated_at}</span>
<span class="badge">Pages {page_count}</span>
<span class="badge">Relations {edge_count}</span>
<span class="badge">Entities {entity_count}</span>
<span class="badge">Aliases {alias_count}</span>
<span class="badge"><a href="index.html">Open Graph</a></span>
<span class="badge"><a href="../index.html">Open Workspace Home</a></span>
</div>
<section>
<h2>Health Summary</h2>
<div class="stats">
{metric_cards}
</div>
</section>
<div class="layout">
<section class="panel">
<h2>Top Actions</h2>
<p>Start with the graph issues that most affect navigation and maintainability.</p>
{top_actions}
</section>
<section class="panel">
<h2>Key Pages</h2>
<p>Read or strengthen these pages first because they matter most to the current graph structure.</p>
<div class="records">{top_nodes}</div>
</section>
<section class="panel">
<h2>Bridge Pages</h2>
<p>These pages connect different node types or topical regions.</p>
<div class="records">{bridge_nodes}</div>
</section>
<section class="panel">
<h2>Pages That Need Links</h2>
<p>This section shows isolated pages and weakly connected pages that should gain links first.</p>
<div class="records">{isolated_pages}</div>
</section>
<section class="panel">
<h2>Entities That Need Links</h2>
<p>These entities have been identified but still lack enough knowledge-graph connections.</p>
<div class="records">{isolated_entities}</div>
</section>
<section class="panel">
<h2>Ambiguous Entity Merge Candidates</h2>
<p>These identity keys match multiple entity pages and should be reviewed before any canonical merge.</p>
<div class="records">{ambiguous_merge_candidates}</div>
</section>
<section class="panel">
<h2>Hub Stubs</h2>
<p>These pages have many relations but thin content, so improving summaries and context usually pays off first.</p>
<div class="records">{hub_stubs}</div>
</section>
<section class="panel">
<h2>Fragile Bridges</h2>
<p>These structures rely on very few pages to keep cross-topic links alive, which makes them fragile.</p>
<div class="records">{fragile_bridges}</div>
</section>
<section class="panel">
<h2>Isolated Clusters</h2>
<p>These pages already form small clusters but are not connected back to the main graph yet.</p>
<div class="records">{isolated_clusters}</div>
</section>
<section class="panel">
<h2>Suggested Links</h2>
<p>These are high-confidence candidate relations that could become explicit links after review.</p>
<div class="records">{suggested_links}</div>
</section>
</div>
</main>
</body>
</html>
""".format(
summary=escape(str(report.get("summary") or "")),
generated_at=escape(str(report.get("generated_at") or "")),
page_count=escape(str(stats.get("nodeCount", 0) or 0)),
edge_count=escape(str(stats.get("edgeCount", 0) or 0)),
entity_count=escape(str(stats.get("entityCount", 0) or 0)),
alias_count=escape(str(stats.get("aliasCount", 0) or 0)),
metric_cards=_render_html_metric_cards(stats),
top_actions=_render_html_list([str(item) for item in top_actions] if isinstance(top_actions, list) else []),
top_nodes=top_node_cards,
bridge_nodes=bridge_node_cards,
isolated_pages=isolated_cards,
isolated_entities=isolated_entity_cards,
ambiguous_merge_candidates=ambiguous_merge_cards,
hub_stubs=hub_cards,
fragile_bridges=bridge_cards,
isolated_clusters=cluster_cards,
suggested_links=suggestion_cards,
)
def main() -> int:
parser = argparse.ArgumentParser(description="Build a deterministic graph health report from the current ThinkWiki graph data.")
parser.add_argument("--root", default=".", help="Wiki root path")
args = parser.parse_args()
root = find_repo_root(Path(args.root))
graph = _load_graph(root)
report = build_report(graph)
report_dir = root / "output" / "graph"
report_json_path = report_dir / "report.json"
report_md_path = report_dir / "report.md"
report_html_path = report_dir / "report.html"
write_text(report_json_path, json.dumps(report, ensure_ascii=False, indent=2))
write_text(report_md_path, render_report_markdown(report))
write_text(report_html_path, render_report_html(report))
output_home = write_output_home(root)
append_log(root, f"[{today_str()}] graph-report | {report['stats']['nodeCount']} pages", [
"- report html: output/graph/report.html",
"- report: output/graph/report.md",
"- data: output/graph/report.json",
"- hub: output/index.html",
])
print("# ThinkWiki Graph Report")
print("")
print(f"- Root: {root}")
print(f"- Summary: {_console_summary(report)}")
print(f"- Isolated Pages: {report['stats']['isolatedPageCount']}")
print(f"- Hub Stubs: {report['stats']['hubStubCount']}")
print(f"- Fragile Bridges: {report['stats']['fragileBridgeCount']}")
print(f"- Suggested Links: {report['stats']['suggestedLinkCount']}")
print("")
print("Top Actions:")
for item in _console_actions(report):
print(f"- {item}")
print("")
print("Graph report: output/graph/report.html")
print(f"Graph report URI: {file_uri(report_html_path)}")
print("Graph report markdown: output/graph/report.md")
print("Graph report data: output/graph/report.json")
print("Output hub: output/index.html")
print(f"Output hub URI: {file_uri(output_home)}")
print_output_serve_hint(root)
return 0
if __name__ == "__main__":
raise SystemExit(main())
98 files · 5 not served here
- .github/workflows/cross-platform.yml
- .gitignore
- ATTRIBUTION.md
- CHANGELOG.md
- LICENSE
- README.md
- README.zh.md
- SKILL.md
- docs/assets/graph-preview.png (not served here)
- docs/assets/graph-report-preview.png (not served here)
- docs/assets/inbox-preview.png (not served here)
- docs/assets/output-hub-preview.png (not served here)
- docs/assets/viewer-preview.png (not served here)
- docs/demo-wiki/.wiki-schema.md
- docs/demo-wiki/index.md
- docs/demo-wiki/log.md
- docs/demo-wiki/normalized/articles/eval-loops.md
- docs/demo-wiki/normalized/articles/platform-spec.md
- docs/demo-wiki/normalized/articles/review-checklist.md
- docs/demo-wiki/normalized/assets/inbox/2026-06-20-team-retrospective/cover.txt
- docs/demo-wiki/normalized/inbox/2026-06-20-team-retrospective.json
- docs/demo-wiki/normalized/inbox/2026-06-20-team-retrospective.md
- docs/demo-wiki/output/graph/graph.json
- docs/demo-wiki/output/graph/graph.md
- docs/demo-wiki/output/graph/index.html
- docs/demo-wiki/output/graph/report.html
- docs/demo-wiki/output/graph/report.json
- docs/demo-wiki/output/graph/report.md
- docs/demo-wiki/output/inbox/index.html
- docs/demo-wiki/output/index.html
- docs/demo-wiki/output/viewer/index.html
- docs/demo-wiki/output/viewer/viewer.json
- docs/demo-wiki/overview.md
- docs/demo-wiki/purpose.md
- docs/demo-wiki/raw/articles/eval-loops.md
- docs/demo-wiki/raw/articles/platform-spec.md
- docs/demo-wiki/raw/articles/review-checklist.md
- docs/demo-wiki/raw/inbox/2026-06-20-team-retrospective.md
- docs/demo-wiki/wiki/concepts/ai-native-team.md
- docs/demo-wiki/wiki/concepts/execution-spec.md
- docs/demo-wiki/wiki/decisions/context-budget.md
- docs/demo-wiki/wiki/decisions/execution-policy.md
- docs/demo-wiki/wiki/queries/what-makes-ai-native-team.md
- docs/demo-wiki/wiki/sources/eval-loops.md
- docs/demo-wiki/wiki/sources/platform-spec.md
- docs/demo-wiki/wiki/sources/review-checklist.md
- docs/demo-wiki/wiki/syntheses/delivery-system.md
- docs/demo-wiki/wiki/topics/eval-loops.md
- docs/demo-wiki/wiki/topics/model-platform.md
- requirements.txt
- scripts/ai_config.py
- scripts/ask.py
- scripts/batch_clip.py
- scripts/batch_ingest.py
- scripts/bootstrap_runtime.py
- scripts/build_demo_wiki.py
- scripts/build_graph.py
- scripts/build_inbox.py
- scripts/build_viewer.py
- scripts/capture_readme_screenshots.py
- scripts/clip.py
- scripts/convert_source.py
- scripts/correct.py
- scripts/crystallize.py
- scripts/digest.py
- scripts/doctor.py
- scripts/embed_client.py
- scripts/entity_merge_apply.py
- scripts/entity_merge_review.py
- scripts/graph_report.py
- scripts/health.py
- scripts/ingest.py
- scripts/init_wiki.py
- scripts/lint_wiki.py
- scripts/llm_client.py
- scripts/query_wiki.py
- scripts/rebuild_index.py
- scripts/runtime_capabilities.py
- scripts/serve_outputs.py
- scripts/status.py
- scripts/thinkwiki
- scripts/url_safety.py
- scripts/utils.py
- scripts/workspace_status.py
- templates/pages/concept.md
- templates/pages/decision.md
- templates/pages/entity.md
- templates/pages/query.md
- templates/pages/source.md
- templates/pages/synthesis.md
- templates/pages/topic.md
- templates/root/.wiki-schema.md
- templates/root/AGENTS.md
- templates/root/index.md
- templates/root/log.md
- templates/root/overview.md
- templates/root/purpose.md
- tests/test_thinkwiki.py
Advantages
- Enables natural language management of a knowledge base without requiring the user to memorize complex CLI commands. evidence: README.md
- Maintains Markdown files as the local source of truth, ensuring data portability and ownership. evidence: README.md
- Provides a rich, interactive content knowledge graph that maps semantic relations like claims and entities rather than just file links. evidence: README.md
Limitations
- Requires a local environment with Python 3 and cannot function in pure cloud-based chat environments without local file or shell access. evidence: README.md
- Agent chat UIs typically cannot render the generated HTML interfaces inline, requiring the use of a separate browser or a local loopback server. evidence: README.md
Security
Risk level: No risk found
This brain runs
Agent instructions that run or fetch something (5)
SKILL.md:If the user wants a new workspace, run `init`.SKILL.md:After `viewer`, `graph`, `inbox`, or `graph-report`, run `serve` when the user wants to browse the results.SKILL.md:If a long-running `serve` process is not practical in the current session, run `serve --print-urls` and tell the user to start `serve` locally, then open the printed workspace URL.SKILL.md:After starting `serve`, give the user the workspace home URL first (`http://127.0.0.1:8765/index.html` by default) and, in OpenClaw, offer to open it with `openclaw browser --browser-profile openclaw open <url>`.· uses the networkSKILL.md:If the host cannot keep a long-running process alive, run `serve --print-urls` to show the URLs and ask the user to start `serve` in a local terminal or background job.
Attribution and licence
- First committed by
wzdavid
- Publisher
- Wei Zhang (Community)
- Source
- wzdavid/ThinkWiki · commit 89f477d
- Licence
- MIT License (files served here)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
- Contributors
- wzdavid
AutoGineer is not affiliated with or endorsed by wzdavid.
Install from source
5 of its files are not served here, so there is no ZIP. wzdavid/ThinkWiki at 89f477d