Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .agents/skills/build-cogmap/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ Flags:
- `--from-onex` — best-effort re-extract `.onex` → clean text first (~66%).
- `--skip-resolve`, `--skip-synth` — force-skip those gates.
- `--no-open` — don't auto-open the finished HTML (also via `COGMAP_NO_OPEN=1`).
- `--publish-github-pages` — after a successful build, publish the visualization
to a `gh-pages` branch and configure GitHub Pages when `gh` is authenticated.
Optional controls: `--publish-remote <name>` (default `origin`),
`--publish-branch <name>` (default `gh-pages`), `--publish-path <path>` for a
subdirectory, `--publish-no-push` for a local dry run, and
`--publish-no-enable-pages` to skip Pages API configuration.

**Default recommendation:** `--with-resolve --with-synth` so a real content change
yields fully-merged concepts and fresh insights. Read `action.json` after every
Expand Down Expand Up @@ -202,6 +208,13 @@ finished `output/knowledge-base-viz.html` is **auto-opened in the default browse
user to **hard-refresh the browser (Ctrl+Shift+R)** — for large corpora the
embedded page can be 1 MB+ and can't be reloaded from the agent side.

If the user asks to share or publish the map publicly, re-run the final refresh
with `--publish-github-pages` from a GitHub-backed repository. The publisher copies
`knowledge-base-viz.html` to `index.html` on the Pages branch, includes the data
JSON and a small manifest, pushes the branch, and prints the Pages URL when it can
parse the GitHub remote. If automatic Pages enablement fails, report the printed
warning and the branch it pushed so the user can enable Pages manually.

## Notes

- First run **seeds the workspace** from the bundled demo (`assets/demo/`) and
Expand Down
209 changes: 209 additions & 0 deletions .agents/skills/build-cogmap/scripts/publish_github_pages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
#!/usr/bin/env python3
"""Publish a built CogMap visualization to a GitHub Pages branch."""
import argparse
import json
import os
import pathlib
import re
import shutil
import subprocess
import sys
import tempfile
from datetime import datetime, timezone


DEFAULT_BRANCH = "gh-pages"


def run(cmd, cwd=None, check=True):
result = subprocess.run(
cmd,
cwd=str(cwd) if cwd else None,
capture_output=True,
text=True,
encoding="utf-8",
)
if check and result.returncode != 0:
detail = (result.stdout + result.stderr).strip()
raise RuntimeError("{} failed{}".format(" ".join(cmd), f":\n{detail}" if detail else ""))
return result


def parse_github_remote(url):
"""Return (owner, repo) for common GitHub HTTPS/SSH remote URL forms."""
patterns = (
r"^https://github\.com/(?P<owner>[^/]+)/(?P<repo>[^/]+?)(?:\.git)?/?$",
r"^git@github\.com:(?P<owner>[^/]+)/(?P<repo>[^/]+?)(?:\.git)?$",
r"^ssh://git@github\.com/(?P<owner>[^/]+)/(?P<repo>[^/]+?)(?:\.git)?/?$",
)
for pattern in patterns:
match = re.match(pattern, url.strip())
if match:
return match.group("owner"), match.group("repo")
return None, None


def pages_url(owner, repo, site_path="."):
if not owner or not repo:
return None
base = f"https://{owner}.github.io/" if repo.lower() == f"{owner.lower()}.github.io" else f"https://{owner}.github.io/{repo}/"
clean = site_path.strip().strip("/").strip(".")
return base if not clean else base + clean + "/"


def copy_payload(output_dir, target_dir):
output_dir = pathlib.Path(output_dir)
target_dir = pathlib.Path(target_dir)
html = output_dir / "knowledge-base-viz.html"
data = output_dir / "knowledge-base-viz-data.json"
if not html.exists():
raise FileNotFoundError(f"missing built visualization: {html}")

target_dir.mkdir(parents=True, exist_ok=True)
shutil.copy2(html, target_dir / "index.html")
if data.exists():
shutil.copy2(data, target_dir / data.name)

(target_dir / ".nojekyll").write_text("", encoding="utf-8")
manifest = {
"name": "CogMap",
"published_at": datetime.now(timezone.utc).isoformat(),
"entrypoint": "index.html",
"source_html": html.name,
"data_file": data.name if data.exists() else None,
}
(target_dir / "cogmap-manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")


def ensure_git_identity(worktree):
name = run(["git", "config", "user.name"], cwd=worktree, check=False).stdout.strip()
email = run(["git", "config", "user.email"], cwd=worktree, check=False).stdout.strip()
if not name:
run(["git", "config", "user.name", "CogMap Publisher"], cwd=worktree)
if not email:
run(["git", "config", "user.email", "cogmap-publisher@users.noreply.github.com"], cwd=worktree)


def ref_exists(repo_root, ref):
return run(["git", "rev-parse", "--verify", "--quiet", ref], cwd=repo_root, check=False).returncode == 0


def add_pages_worktree(repo_root, worktree, remote, branch):
run(["git", "fetch", remote, branch, "--depth=1"], cwd=repo_root, check=False)
remote_ref = f"refs/remotes/{remote}/{branch}"
local_ref = f"refs/heads/{branch}"
if ref_exists(repo_root, remote_ref):
run(["git", "worktree", "add", "-B", branch, str(worktree), f"{remote}/{branch}"], cwd=repo_root)
return
if ref_exists(repo_root, local_ref):
run(["git", "worktree", "add", str(worktree), branch], cwd=repo_root)
return

run(["git", "worktree", "add", "--detach", str(worktree), "HEAD"], cwd=repo_root)
run(["git", "checkout", "--orphan", branch], cwd=worktree)
run(["git", "rm", "-rf", "."], cwd=worktree, check=False)
for child in worktree.iterdir():
if child.name == ".git":
continue
if child.is_dir():
shutil.rmtree(child)
else:
child.unlink()


def enable_pages(owner, repo, branch):
if not owner or not repo:
return "Skipped Pages enablement: remote is not a github.com repository."
if shutil.which("gh") is None:
return "Skipped Pages enablement: GitHub CLI (`gh`) is not installed."

endpoint = f"repos/{owner}/{repo}/pages"
get_result = run(["gh", "api", endpoint], check=False)
verb = "PUT" if get_result.returncode == 0 else "POST"
result = run(
[
"gh",
"api",
"-X",
verb,
endpoint,
"-f",
f"source[branch]={branch}",
"-f",
"source[path]=/",
],
check=False,
)
if result.returncode != 0:
detail = (result.stdout + result.stderr).strip()
return "Pages branch was pushed, but automatic Pages enablement failed: {}".format(detail or "unknown error")
return "GitHub Pages is configured to serve from {}/.".format(branch)


def publish(args):
repo_root = pathlib.Path(args.repo_root or ".").resolve()
if not (repo_root / ".git").exists():
repo_root = pathlib.Path(run(["git", "rev-parse", "--show-toplevel"], cwd=repo_root).stdout.strip())

remote_url = run(["git", "remote", "get-url", args.remote], cwd=repo_root, check=False).stdout.strip()
owner, repo = parse_github_remote(remote_url) if remote_url else (None, None)
tmp = pathlib.Path(tempfile.mkdtemp(prefix="cogmap-pages-"))
worktree = tmp / "worktree"
committed = False
try:
add_pages_worktree(repo_root, worktree, args.remote, args.branch)
target = worktree if args.site_path in ("", ".", "/") else worktree / args.site_path.strip("/\\")
copy_payload(args.output, target)
run(["git", "add", "-A"], cwd=worktree)
if run(["git", "diff", "--cached", "--quiet"], cwd=worktree, check=False).returncode == 0:
print("GitHub Pages branch already matches the current CogMap output.")
else:
ensure_git_identity(worktree)
run(["git", "commit", "-m", "Publish CogMap visualization"], cwd=worktree)
committed = True
print("Committed CogMap output to {}.".format(args.branch))

if args.no_push:
print("Published locally to branch {} (--no-push).".format(args.branch))
else:
if not remote_url:
raise RuntimeError(f"remote `{args.remote}` was not found")
run(["git", "push", args.remote, args.branch], cwd=worktree)
print("Pushed CogMap output to {}/{}.".format(args.remote, args.branch))
if not args.no_enable_pages:
print(enable_pages(owner, repo, args.branch))

url = pages_url(owner, repo, args.site_path)
if url:
print("CogMap Pages URL: {}".format(url))
elif args.no_push:
print("No Pages URL available until the branch is pushed to a github.com remote.")
return committed
finally:
run(["git", "worktree", "remove", "--force", str(worktree)], cwd=repo_root, check=False)
shutil.rmtree(tmp, ignore_errors=True)


def parse_args(argv):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", required=True, help="Directory containing knowledge-base-viz.html.")
parser.add_argument("--repo-root", default=".", help="Git repository to publish from.")
parser.add_argument("--remote", default="origin", help="Git remote to push to.")
parser.add_argument("--branch", default=DEFAULT_BRANCH, help="Pages branch to create/update.")
parser.add_argument("--site-path", default=".", help="Optional subdirectory within the Pages branch.")
parser.add_argument("--no-push", action="store_true", help="Commit locally but do not push.")
parser.add_argument("--no-enable-pages", action="store_true", help="Do not configure Pages with gh.")
return parser.parse_args(argv)


def main(argv=None):
try:
publish(parse_args(argv or sys.argv[1:]))
return 0
except Exception as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 1


if __name__ == "__main__":
sys.exit(main())
39 changes: 39 additions & 0 deletions .agents/skills/build-cogmap/scripts/refresh.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
--with-synth re-run insight synthesis when the graph changed
--skip-resolve reuse existing resolution even if stale (new concepts -> singletons)
--skip-synth reuse existing insights even if stale
--publish-github-pages
publish the finished HTML to a GitHub Pages branch
"""
import json, re, hashlib, pathlib, subprocess, sys, os, time, shutil
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
Expand All @@ -34,6 +36,18 @@
PY = sys.executable
ARGS = set(sys.argv[1:])


def arg_value(name, default):
flag = f'--{name}'
prefix = flag + '='
argv = sys.argv[1:]
for i, value in enumerate(argv):
if value == flag and i + 1 < len(argv):
return argv[i + 1]
if value.startswith(prefix):
return value[len(prefix):]
return default

def sha1s(s): return hashlib.sha1(s.encode('utf-8', 'ignore')).hexdigest()

def safe_rmtree(p):
Expand Down Expand Up @@ -98,6 +112,28 @@ def open_result(html):
print('Open it manually: {}'.format(html))


def publish_github_pages():
cmd = [PY, str(PIPELINE / 'publish_github_pages.py'),
'--output', str(OUTPUT),
'--repo-root', str(pathlib.Path.cwd()),
'--remote', arg_value('publish-remote', 'origin'),
'--branch', arg_value('publish-branch', 'gh-pages'),
'--site-path', arg_value('publish-path', '.')]
if '--publish-no-push' in ARGS:
cmd.append('--no-push')
if '--publish-no-enable-pages' in ARGS:
cmd.append('--no-enable-pages')
env = dict(os.environ); env['PYTHONIOENCODING'] = 'utf-8'
r = subprocess.run(cmd, env=env, capture_output=True, text=True, encoding='utf-8')
if r.stdout.strip():
print(r.stdout.strip())
if r.returncode != 0:
if r.stderr.strip():
print(r.stderr.strip())
print('--- publish_github_pages.py FAILED ---')
sys.exit(r.returncode)


def load_state():
if STATEF.exists():
return json.loads(STATEF.read_text(encoding='utf-8'))
Expand Down Expand Up @@ -403,6 +439,9 @@ def main():
save_state(state)
print('\nDONE. chunks={} tracked-extracted={}'.format(len(current), len(state['extracted_ids'])))
print('Hard-refresh the browser (Ctrl+Shift+R) to see the update.')
if '--publish-github-pages' in ARGS:
print('publish: GitHub Pages...')
publish_github_pages()
open_result(OUTPUT / 'knowledge-base-viz.html')

if __name__ == '__main__':
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ evolving landscape of thought.
- Rebuilds incrementally, re-processing only changed chunks where possible.
- Ships with a GitHub Copilot / Claude Code skill for agent-assisted refreshes.
- Outputs a portable HTML artifact with embedded data and no server requirement.
- Can publish the generated artifact to GitHub Pages for easy sharing.

## See it in action

Expand Down Expand Up @@ -112,6 +113,11 @@ pipeline, gives the coding agent structured actions to perform, and then resumes
That agent-in-the-loop loop is what lets CogMap rebuild without requiring a
separate LLM API key.

To share the finished map from a GitHub-backed repository, ask the agent to
publish it to GitHub Pages. The skill runs `refresh.py --publish-github-pages`,
copies the visualization to `index.html` on a `gh-pages` branch, pushes it, and
prints the Pages URL.

## Installation paths

### GitHub Copilot CLI
Expand Down
4 changes: 4 additions & 0 deletions cogmap-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ cogmap-app/ Claude Code plugin root
│ ├─ v3_aggregate.py merge extractions → raw concepts
│ ├─ v3_assemble.py build the graph data JSON
│ ├─ build_v2.py render the self-contained HTML
│ ├─ publish_github_pages.py optional: publish output to GitHub Pages
│ └─ extract_onex.py optional: OneNote .onex → clean text (~66%)
└─ assets/demo/ bundled demo corpus + prebuilt artifacts
├─ sources/sample-notes.txt synthetic demo corpus
Expand Down Expand Up @@ -86,6 +87,9 @@ CogMap is a **coding agent skill**, not a standalone app you run by hand:
it writes them into `cogmap/sources/`, removes the demo corpus, and refreshes.
4. Open `cogmap/output/knowledge-base-viz.html` when the agent reports completion
(the agent prints the exact workspace path on every run).
5. To share the result from a GitHub-backed repo, ask the agent to publish it to
GitHub Pages. The skill runs `refresh.py --publish-github-pages`, updates the
`gh-pages` branch with `index.html`, and prints the Pages URL.

`refresh.py` diffs the notes, re-extracts **only the chunks that changed**,
re-clusters **only new concepts**, re-synthesizes insights **only if the graph
Expand Down
13 changes: 13 additions & 0 deletions cogmap-app/skills/build-cogmap/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ Flags:
- `--from-onex` — best-effort re-extract `.onex` → clean text first (~66%).
- `--skip-resolve`, `--skip-synth` — force-skip those gates.
- `--no-open` — don't auto-open the finished HTML (also via `COGMAP_NO_OPEN=1`).
- `--publish-github-pages` — after a successful build, publish the visualization
to a `gh-pages` branch and configure GitHub Pages when `gh` is authenticated.
Optional controls: `--publish-remote <name>` (default `origin`),
`--publish-branch <name>` (default `gh-pages`), `--publish-path <path>` for a
subdirectory, `--publish-no-push` for a local dry run, and
`--publish-no-enable-pages` to skip Pages API configuration.

**Default recommendation:** `--with-resolve --with-synth` so a real content change
yields fully-merged concepts and fresh insights. Read `action.json` after every
Expand Down Expand Up @@ -202,6 +208,13 @@ finished `output/knowledge-base-viz.html` is **auto-opened in the default browse
user to **hard-refresh the browser (Ctrl+Shift+R)** — for large corpora the
embedded page can be 1 MB+ and can't be reloaded from the agent side.

If the user asks to share or publish the map publicly, re-run the final refresh
with `--publish-github-pages` from a GitHub-backed repository. The publisher copies
`knowledge-base-viz.html` to `index.html` on the Pages branch, includes the data
JSON and a small manifest, pushes the branch, and prints the Pages URL when it can
parse the GitHub remote. If automatic Pages enablement fails, report the printed
warning and the branch it pushed so the user can enable Pages manually.

## Notes

- First run **seeds the workspace** from the bundled demo (`assets/demo/`) and
Expand Down
Loading
Loading