Agent instructions, skills, SSH host aliases and expansion triggers are worth having identical on every machine one person owns, and belong in none of the shared configuration. They live in user/ now, with a manifest saying where each piece goes and a link-user stage that puts it there. That stage does nothing unless the machine said yes. Somebody who clones Panama to try the desktop keeps their own ~/.claude/CLAUDE.md exactly where it was; the question names the destinations and defaults to no. Anything displaced goes to config/old rather than being deleted. ~/.claude/CLAUDE.md and ~/.codex/AGENTS.md were byte-identical copies of one file, which is the drift this exists to prevent. Also adds the vitals toggles for the battery and Claude usage readouts, which had preferences and no way to reach them.
164 lines
5.4 KiB
Python
164 lines
5.4 KiB
Python
import json
|
|
import re
|
|
import sys
|
|
|
|
INLINE_CODE = re.compile(r"`([^`]+)`")
|
|
BOLD = re.compile(r"\*\*([^*]+)\*\*")
|
|
|
|
|
|
def text_node(text, marks=None):
|
|
node = {"type": "text", "text": text}
|
|
if marks:
|
|
node["marks"] = [{"type": m} for m in marks]
|
|
return node
|
|
|
|
|
|
def inline_nodes(text):
|
|
nodes = []
|
|
pos = 0
|
|
pattern = re.compile(r"`([^`]+)`|\*\*([^*]+)\*\*")
|
|
for m in pattern.finditer(text):
|
|
if m.start() > pos:
|
|
nodes.append(text_node(text[pos:m.start()]))
|
|
if m.group(1) is not None:
|
|
nodes.append(text_node(m.group(1), ["code"]))
|
|
else:
|
|
nodes.append(text_node(m.group(2), ["strong"]))
|
|
pos = m.end()
|
|
if pos < len(text):
|
|
nodes.append(text_node(text[pos:]))
|
|
return nodes or [text_node("")]
|
|
|
|
|
|
def paragraph(text):
|
|
return {"type": "paragraph", "content": inline_nodes(text)}
|
|
|
|
|
|
def heading(level, text):
|
|
return {"type": "heading", "attrs": {"level": level}, "content": inline_nodes(text)}
|
|
|
|
|
|
def code_block(lang, lines):
|
|
attrs = {"language": lang} if lang else {}
|
|
return {"type": "codeBlock", "attrs": attrs, "content": [text_node("\n".join(lines))]}
|
|
|
|
|
|
def list_node(items, ordered):
|
|
return {
|
|
"type": "orderedList" if ordered else "bulletList",
|
|
"content": [{"type": "listItem", "content": [paragraph(i)]} for i in items],
|
|
}
|
|
|
|
|
|
def cell(kind, text):
|
|
return {"type": kind, "attrs": {}, "content": [paragraph(text)]}
|
|
|
|
|
|
def table(rows):
|
|
out = {"type": "table", "attrs": {"layout": "default"}, "content": []}
|
|
for i, row in enumerate(rows):
|
|
kind = "tableHeader" if i == 0 else "tableCell"
|
|
out["content"].append({"type": "tableRow", "content": [cell(kind, c) for c in row]})
|
|
return out
|
|
|
|
|
|
def split_row(line):
|
|
return [c.strip() for c in line.strip().strip("|").split("|")]
|
|
|
|
|
|
def build_adf(md):
|
|
lines = md.splitlines()
|
|
content = []
|
|
i = 0
|
|
while i < len(lines):
|
|
line = lines[i]
|
|
if not line.strip():
|
|
i += 1
|
|
continue
|
|
if line.startswith("```"):
|
|
lang = line[3:].strip()
|
|
body = []
|
|
i += 1
|
|
while i < len(lines) and not lines[i].startswith("```"):
|
|
body.append(lines[i])
|
|
i += 1
|
|
i += 1
|
|
content.append(code_block(lang, body))
|
|
continue
|
|
m = re.match(r"^(#{1,6})\s+(.*)", line)
|
|
if m:
|
|
content.append(heading(len(m.group(1)), m.group(2).strip()))
|
|
i += 1
|
|
continue
|
|
if line.lstrip().startswith("|"):
|
|
rows = []
|
|
while i < len(lines) and lines[i].lstrip().startswith("|"):
|
|
if not re.match(r"^\s*\|[\s:|-]+\|\s*$", lines[i]):
|
|
rows.append(split_row(lines[i]))
|
|
i += 1
|
|
content.append(table(rows))
|
|
continue
|
|
if re.match(r"^\s*- ", line):
|
|
items = []
|
|
while i < len(lines) and re.match(r"^\s*- ", lines[i]):
|
|
items.append(re.sub(r"^\s*- ", "", lines[i]).strip())
|
|
i += 1
|
|
content.append(list_node(items, ordered=False))
|
|
continue
|
|
if re.match(r"^\s*\d+\.\s", line):
|
|
items = []
|
|
while i < len(lines) and re.match(r"^\s*\d+\.\s", lines[i]):
|
|
items.append(re.sub(r"^\s*\d+\.\s", "", lines[i]).strip())
|
|
i += 1
|
|
content.append(list_node(items, ordered=True))
|
|
continue
|
|
para = [line.strip()]
|
|
i += 1
|
|
while i < len(lines) and lines[i].strip() and not re.match(r"^(#|```|\s*-\s|\s*\d+\.\s|\s*\|)", lines[i]):
|
|
para.append(lines[i].strip())
|
|
i += 1
|
|
content.append(paragraph(" ".join(para)))
|
|
return {"type": "doc", "version": 1, "content": content}
|
|
|
|
|
|
def append_table_rows(field_json_path, rows_md_path):
|
|
field = json.load(open(field_json_path))
|
|
rows = []
|
|
for line in open(rows_md_path).read().splitlines():
|
|
if line.lstrip().startswith("|") and not re.match(r"^\s*\|[\s:|-]+\|\s*$", line):
|
|
rows.append(split_row(line))
|
|
tables = [n for n in field.get("content", []) if n.get("type") == "table"]
|
|
if not tables:
|
|
raise SystemExit("no table found in the existing field, refusing to guess")
|
|
target = tables[-1]
|
|
header_cells = len(target["content"][0]["content"])
|
|
body_rows = [r for r in target["content"][1:] if any(
|
|
t.strip() for c in r["content"] for t in _cell_texts(c))]
|
|
target["content"] = [target["content"][0]] + body_rows
|
|
for row in rows:
|
|
if len(row) != header_cells:
|
|
raise SystemExit(f"row has {len(row)} cells, table header has {header_cells}: {row}")
|
|
target["content"].append({"type": "tableRow", "content": [cell("tableCell", c) for c in row]})
|
|
return field
|
|
|
|
|
|
def _cell_texts(cell_node):
|
|
out = []
|
|
def walk(n):
|
|
if isinstance(n, dict):
|
|
if n.get("type") == "text":
|
|
out.append(n.get("text", ""))
|
|
for c in n.get("content", []):
|
|
walk(c)
|
|
walk(cell_node)
|
|
return out
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) >= 3 and sys.argv[1] == "render":
|
|
json.dump(build_adf(open(sys.argv[2]).read()), sys.stdout)
|
|
elif len(sys.argv) >= 4 and sys.argv[1] == "tables":
|
|
json.dump(append_table_rows(sys.argv[2], sys.argv[3]), sys.stdout)
|
|
else:
|
|
sys.exit("usage: review2adf.py render <review.md> | tables <current-field.json> <rows.md>")
|