opencode-skills-collection 3.0.39 → 3.0.40
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.
- package/bundled-skills/.antigravity-install-manifest.json +6 -1
- package/bundled-skills/2slides-ppt-generator/SKILL.md +12 -2
- package/bundled-skills/2slides-ppt-generator/requirements.txt +1 -0
- package/bundled-skills/2slides-ppt-generator/scripts/create_pdf_slides.py +1 -1
- package/bundled-skills/accesslint-diff/SKILL.md +4 -1
- package/bundled-skills/article-illustrations/SKILL.md +159 -0
- package/bundled-skills/cv-generator/SKILL.md +874 -0
- package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
- package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
- package/bundled-skills/docs/maintainers/repo-growth-seo.md +3 -3
- package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
- package/bundled-skills/docs/sources/sources.md +1 -0
- package/bundled-skills/docs/users/bundles.md +1 -1
- package/bundled-skills/docs/users/claude-code-skills.md +1 -1
- package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
- package/bundled-skills/docs/users/getting-started.md +1 -1
- package/bundled-skills/docs/users/kiro-integration.md +1 -1
- package/bundled-skills/docs/users/usage.md +4 -4
- package/bundled-skills/docs/users/visual-guide.md +4 -4
- package/bundled-skills/examprep-ai/SKILL.md +8 -0
- package/bundled-skills/hugging-face-cli/SKILL.md +2 -2
- package/bundled-skills/open-dynamic-workflows/SKILL.md +101 -0
- package/bundled-skills/permission-manager/README.md +1 -1
- package/bundled-skills/permission-manager/SKILL.md +2 -1
- package/bundled-skills/polis-protocol/SKILL.md +13 -4
- package/bundled-skills/runapi-cli/SKILL.md +140 -0
- package/bundled-skills/schema-markup-generator/SKILL.md +2 -1
- package/bundled-skills/smart-git-automation/SKILL.md +4 -2
- package/bundled-skills/user-thoughts/scripts/common.py +93 -6
- package/bundled-skills/user-thoughts/scripts/ignore_ops.py +20 -20
- package/bundled-skills/user-thoughts/scripts/init.py +3 -1
- package/bundled-skills/user-thoughts/scripts/show_mdbase.py +5 -5
- package/bundled-skills/user-thoughts/scripts/show_raw.py +3 -3
- package/bundled-skills/user-thoughts/scripts/sortin.py +37 -26
- package/bundled-skills/user-thoughts/scripts/status.py +8 -8
- package/bundled-skills/user-thoughts/scripts/write_raw.py +20 -11
- package/bundled-skills/vercel-cli-with-tokens/SKILL.md +15 -12
- package/bundled-skills/video-content-extractor/SKILL.md +103 -0
- package/package.json +1 -1
- package/skills_index.json +119 -7
|
@@ -3,13 +3,20 @@ import re
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
class UsthtSafetyError(RuntimeError):
|
|
7
|
+
"""Raised when a runtime path would escape .ustht/."""
|
|
8
|
+
|
|
9
|
+
|
|
6
10
|
def find_ustht() -> Path | None:
|
|
7
11
|
"""Find .ustht/ in the current directory or one of its parents."""
|
|
8
12
|
cwd = Path.cwd()
|
|
9
13
|
for d in [cwd, *cwd.parents]:
|
|
10
14
|
ustht = d / ".ustht"
|
|
11
|
-
if ustht.
|
|
12
|
-
|
|
15
|
+
if ustht.exists():
|
|
16
|
+
if ustht.is_symlink():
|
|
17
|
+
raise UsthtSafetyError(f"Refusing symlinked runtime directory: {ustht}")
|
|
18
|
+
if ustht.is_dir():
|
|
19
|
+
return ustht.resolve()
|
|
13
20
|
return None
|
|
14
21
|
|
|
15
22
|
|
|
@@ -28,7 +35,7 @@ def read_define_ini(ustht: Path) -> dict:
|
|
|
28
35
|
if not ini.exists():
|
|
29
36
|
return {}
|
|
30
37
|
result = {}
|
|
31
|
-
for line in ini
|
|
38
|
+
for line in safe_read_text(ustht, ini).splitlines():
|
|
32
39
|
line = line.strip()
|
|
33
40
|
if "=" in line and not line.startswith("#"):
|
|
34
41
|
k, v = line.split("=", 1)
|
|
@@ -40,15 +47,95 @@ def write_define_ini(ustht: Path, cfg: dict):
|
|
|
40
47
|
"""Replace define.ini with the provided key/value pairs."""
|
|
41
48
|
ini = ustht / "define.ini"
|
|
42
49
|
lines = [f"{k}={v}" for k, v in cfg.items()]
|
|
43
|
-
ini
|
|
50
|
+
safe_write_text(ustht, ini, "\n".join(lines) + "\n")
|
|
44
51
|
|
|
45
52
|
|
|
46
|
-
def is_processed(filepath: Path) -> bool:
|
|
53
|
+
def is_processed(filepath: Path, ustht: Path | None = None) -> bool:
|
|
47
54
|
"""Return true when the first raw-file line is the processed marker."""
|
|
48
|
-
|
|
55
|
+
content = safe_read_text(ustht, filepath) if ustht else filepath.read_text(encoding="utf-8")
|
|
56
|
+
first_line = content.split("\n", 1)[0].strip()
|
|
49
57
|
return first_line == "<!-- processed -->"
|
|
50
58
|
|
|
51
59
|
|
|
60
|
+
def ensure_runtime_path(ustht: Path, path: Path, *, must_exist: bool = False) -> Path:
|
|
61
|
+
"""Return a path only if its real location stays inside .ustht/."""
|
|
62
|
+
base = Path(ustht)
|
|
63
|
+
if base.is_symlink():
|
|
64
|
+
raise UsthtSafetyError(f"Refusing symlinked runtime directory: {base}")
|
|
65
|
+
base_real = base.resolve(strict=True)
|
|
66
|
+
target = Path(path)
|
|
67
|
+
if not target.is_absolute():
|
|
68
|
+
target = base_real / target
|
|
69
|
+
|
|
70
|
+
if target.exists() and target.is_symlink():
|
|
71
|
+
raise UsthtSafetyError(f"Refusing symlinked runtime path: {target}")
|
|
72
|
+
if must_exist and not target.exists():
|
|
73
|
+
raise UsthtSafetyError(f"Runtime path does not exist: {target}")
|
|
74
|
+
|
|
75
|
+
target_real = target.resolve(strict=must_exist)
|
|
76
|
+
try:
|
|
77
|
+
target_real.relative_to(base_real)
|
|
78
|
+
except ValueError as exc:
|
|
79
|
+
raise UsthtSafetyError(f"Runtime path escapes .ustht/: {target}") from exc
|
|
80
|
+
|
|
81
|
+
rel = target.relative_to(base_real)
|
|
82
|
+
current = base_real
|
|
83
|
+
for part in rel.parts:
|
|
84
|
+
current = current / part
|
|
85
|
+
if current.exists() and current.is_symlink():
|
|
86
|
+
raise UsthtSafetyError(f"Refusing symlinked runtime path: {current}")
|
|
87
|
+
return target
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def ensure_runtime_dir(ustht: Path, path: Path, *, create: bool = False) -> Path:
|
|
91
|
+
"""Return a safe runtime directory, creating it when requested."""
|
|
92
|
+
directory = ensure_runtime_path(ustht, path, must_exist=False)
|
|
93
|
+
if create:
|
|
94
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
if directory.exists() and not directory.is_dir():
|
|
96
|
+
raise UsthtSafetyError(f"Runtime path is not a directory: {directory}")
|
|
97
|
+
return directory
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def safe_read_text(ustht: Path | None, path: Path) -> str:
|
|
101
|
+
"""Read a runtime file after symlink and containment checks."""
|
|
102
|
+
safe_path = ensure_runtime_path(ustht, path, must_exist=True) if ustht else path
|
|
103
|
+
return safe_path.read_text(encoding="utf-8")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def safe_write_text(ustht: Path, path: Path, content: str):
|
|
107
|
+
"""Write a runtime file after symlink and containment checks."""
|
|
108
|
+
safe_path = ensure_runtime_path(ustht, path, must_exist=False)
|
|
109
|
+
ensure_runtime_dir(ustht, safe_path.parent, create=True)
|
|
110
|
+
safe_path.write_text(content, encoding="utf-8")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def safe_markdown_files(ustht: Path, directory: Path, *, reverse: bool = False) -> list[Path]:
|
|
114
|
+
"""List safe markdown files under one runtime directory."""
|
|
115
|
+
safe_dir = ensure_runtime_dir(ustht, directory)
|
|
116
|
+
if not safe_dir.exists():
|
|
117
|
+
return []
|
|
118
|
+
files = []
|
|
119
|
+
for file_path in safe_dir.glob("*.md"):
|
|
120
|
+
safe_path = ensure_runtime_path(ustht, file_path, must_exist=True)
|
|
121
|
+
if safe_path.is_file():
|
|
122
|
+
files.append(safe_path)
|
|
123
|
+
return sorted(files, reverse=reverse)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def safe_markdown_tree(ustht: Path, directory: Path) -> list[Path]:
|
|
127
|
+
"""List safe markdown files recursively under one runtime directory."""
|
|
128
|
+
safe_dir = ensure_runtime_dir(ustht, directory)
|
|
129
|
+
if not safe_dir.exists():
|
|
130
|
+
return []
|
|
131
|
+
files = []
|
|
132
|
+
for file_path in safe_dir.rglob("*.md"):
|
|
133
|
+
safe_path = ensure_runtime_path(ustht, file_path, must_exist=True)
|
|
134
|
+
if safe_path.is_file():
|
|
135
|
+
files.append(safe_path)
|
|
136
|
+
return sorted(files)
|
|
137
|
+
|
|
138
|
+
|
|
52
139
|
def validate_dim_name(dim: str) -> bool:
|
|
53
140
|
"""Validate a dimension path made of safe kebab-case segments."""
|
|
54
141
|
reserved = {"raw", "ignored", "export", "define", "readme-ai"}
|
|
@@ -3,7 +3,7 @@ import sys
|
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
-
from common import find_ustht
|
|
6
|
+
from common import ensure_runtime_dir, find_ustht, safe_markdown_files, safe_read_text, safe_write_text
|
|
7
7
|
|
|
8
8
|
HELP = """Usage: python ignore_ops.py show|remove_last|add_suffix "text" [--help]
|
|
9
9
|
|
|
@@ -14,11 +14,11 @@ Subcommands:
|
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def find_last_raw_entry(raw_dir: Path):
|
|
17
|
+
def find_last_raw_entry(ustht: Path, raw_dir: Path):
|
|
18
18
|
"""Return (file path, line index, entry text) for the latest raw entry."""
|
|
19
|
-
files =
|
|
19
|
+
files = safe_markdown_files(ustht, raw_dir, reverse=True)
|
|
20
20
|
for f in files:
|
|
21
|
-
lines = f
|
|
21
|
+
lines = safe_read_text(ustht, f).splitlines()
|
|
22
22
|
if lines and lines[0].strip() == "<!-- processed -->":
|
|
23
23
|
continue
|
|
24
24
|
for idx in range(len(lines) - 1, -1, -1):
|
|
@@ -27,16 +27,16 @@ def find_last_raw_entry(raw_dir: Path):
|
|
|
27
27
|
return None, None, None
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
def remove_line(filepath: Path, idx: int):
|
|
30
|
+
def remove_line(ustht: Path, filepath: Path, idx: int):
|
|
31
31
|
"""Remove one line from a file."""
|
|
32
|
-
lines = filepath
|
|
32
|
+
lines = safe_read_text(ustht, filepath).splitlines()
|
|
33
33
|
del lines[idx]
|
|
34
|
-
filepath
|
|
34
|
+
safe_write_text(ustht, filepath, "\n".join(lines) + ("\n" if lines else ""))
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
def append_to_ignored(ignored_dir: Path, text: str, reason: str):
|
|
37
|
+
def append_to_ignored(ustht: Path, ignored_dir: Path, text: str, reason: str):
|
|
38
38
|
"""Append one ignored entry to today's ignored file."""
|
|
39
|
-
ignored_dir
|
|
39
|
+
ignored_dir = ensure_runtime_dir(ustht, ignored_dir, create=True)
|
|
40
40
|
today = datetime.now().strftime("%Y-%m-%d")
|
|
41
41
|
now = datetime.now().strftime("%H:%M")
|
|
42
42
|
f = ignored_dir / f"{today}.md"
|
|
@@ -45,23 +45,23 @@ def append_to_ignored(ignored_dir: Path, text: str, reason: str):
|
|
|
45
45
|
clean = clean.rsplit(" | suggested-dim:", 1)[0]
|
|
46
46
|
entry = f"- [{now}] {clean} ({reason})"
|
|
47
47
|
if f.exists():
|
|
48
|
-
content = f
|
|
49
|
-
|
|
48
|
+
content = safe_read_text(ustht, f).rstrip()
|
|
49
|
+
safe_write_text(ustht, f, f"{content}\n{entry}\n")
|
|
50
50
|
else:
|
|
51
|
-
|
|
51
|
+
safe_write_text(ustht, f, f"{entry}\n")
|
|
52
52
|
|
|
53
53
|
|
|
54
|
-
def show_ignored(ignored_dir: Path):
|
|
54
|
+
def show_ignored(ustht: Path, ignored_dir: Path):
|
|
55
55
|
"""Print all ignored entries."""
|
|
56
56
|
if not ignored_dir.exists():
|
|
57
57
|
print("No ignored entries.")
|
|
58
58
|
return
|
|
59
|
-
files =
|
|
59
|
+
files = safe_markdown_files(ustht, ignored_dir, reverse=True)
|
|
60
60
|
if not files:
|
|
61
61
|
print("No ignored entries.")
|
|
62
62
|
return
|
|
63
63
|
for f in files:
|
|
64
|
-
entries = [line for line in f
|
|
64
|
+
entries = [line for line in safe_read_text(ustht, f).splitlines() if line.strip().startswith("- [")]
|
|
65
65
|
if entries:
|
|
66
66
|
print(f"#{f.name} ({len(entries)} entries):")
|
|
67
67
|
for entry in entries:
|
|
@@ -73,12 +73,12 @@ def remove_last(ustht: Path):
|
|
|
73
73
|
if not raw_dir.exists():
|
|
74
74
|
print("No previous thought to ignore.")
|
|
75
75
|
return
|
|
76
|
-
filepath, idx, entry = find_last_raw_entry(raw_dir)
|
|
76
|
+
filepath, idx, entry = find_last_raw_entry(ustht, raw_dir)
|
|
77
77
|
if filepath is None:
|
|
78
78
|
print("No previous thought to ignore.")
|
|
79
79
|
return
|
|
80
|
-
remove_line(filepath, idx)
|
|
81
|
-
append_to_ignored(ustht / "ignored", entry, "ignored with --last")
|
|
80
|
+
remove_line(ustht, filepath, idx)
|
|
81
|
+
append_to_ignored(ustht, ustht / "ignored", entry, "ignored with --last")
|
|
82
82
|
display = entry
|
|
83
83
|
if "] " in display:
|
|
84
84
|
display = display.split("] ", 1)[1]
|
|
@@ -88,7 +88,7 @@ def remove_last(ustht: Path):
|
|
|
88
88
|
|
|
89
89
|
|
|
90
90
|
def add_suffix(ustht: Path, text: str):
|
|
91
|
-
append_to_ignored(ustht / "ignored", text, "ignored by suffix")
|
|
91
|
+
append_to_ignored(ustht, ustht / "ignored", text, "ignored by suffix")
|
|
92
92
|
print("Ignored current message.")
|
|
93
93
|
|
|
94
94
|
|
|
@@ -108,7 +108,7 @@ def main():
|
|
|
108
108
|
|
|
109
109
|
cmd = sys.argv[1]
|
|
110
110
|
if cmd == "show":
|
|
111
|
-
show_ignored(ustht / "ignored")
|
|
111
|
+
show_ignored(ustht, ustht / "ignored")
|
|
112
112
|
elif cmd == "remove_last":
|
|
113
113
|
remove_last(ustht)
|
|
114
114
|
elif cmd == "add_suffix":
|
|
@@ -3,7 +3,7 @@ import shutil
|
|
|
3
3
|
import sys
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
-
from common import find_skill_dir
|
|
6
|
+
from common import UsthtSafetyError, find_skill_dir
|
|
7
7
|
|
|
8
8
|
HELP = """Usage: python init.py [--help]
|
|
9
9
|
|
|
@@ -34,6 +34,8 @@ def main():
|
|
|
34
34
|
|
|
35
35
|
target = Path.cwd() / ".ustht"
|
|
36
36
|
if target.exists():
|
|
37
|
+
if target.is_symlink():
|
|
38
|
+
raise UsthtSafetyError(f"Refusing symlinked runtime directory: {target}")
|
|
37
39
|
print("Already initialized; .ustht/ exists, skipping creation.")
|
|
38
40
|
sys.exit(0)
|
|
39
41
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import sys
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
-
from common import find_ustht, validate_dim_name
|
|
5
|
+
from common import find_ustht, safe_markdown_tree, safe_read_text, validate_dim_name
|
|
6
6
|
|
|
7
7
|
HELP = """Usage: python show_mdbase.py show [--all|--dimension] [--help]
|
|
8
8
|
|
|
@@ -18,14 +18,14 @@ def show_index(mdbase: Path):
|
|
|
18
18
|
if not index.exists():
|
|
19
19
|
print("mdbase/README.ai.md does not exist.")
|
|
20
20
|
return
|
|
21
|
-
print(index
|
|
21
|
+
print(safe_read_text(mdbase.parent, index))
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def list_dims(mdbase: Path):
|
|
25
25
|
details = mdbase / "details"
|
|
26
26
|
if not details.exists():
|
|
27
27
|
return []
|
|
28
|
-
return sorted(p.relative_to(details).with_suffix("").as_posix() for p in details
|
|
28
|
+
return sorted(p.relative_to(details).with_suffix("").as_posix() for p in safe_markdown_tree(mdbase.parent, details))
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def show_dim(mdbase: Path, dim: str):
|
|
@@ -39,7 +39,7 @@ def show_dim(mdbase: Path, dim: str):
|
|
|
39
39
|
if not path.exists():
|
|
40
40
|
print(f"mdbase/details/{dim}.md does not exist yet.")
|
|
41
41
|
return
|
|
42
|
-
print(path
|
|
42
|
+
print(safe_read_text(mdbase.parent, path))
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
def show_all(mdbase: Path):
|
|
@@ -54,7 +54,7 @@ def show_all(mdbase: Path):
|
|
|
54
54
|
print(f"mdbase has {len(dims)} dimensions:")
|
|
55
55
|
for dim in dims:
|
|
56
56
|
path = details / f"{dim}.md"
|
|
57
|
-
lines = [line for line in path
|
|
57
|
+
lines = [line for line in safe_read_text(mdbase.parent, path).splitlines() if line.strip().startswith("- ")]
|
|
58
58
|
print(f" {dim}.md: {len(lines)} entries")
|
|
59
59
|
|
|
60
60
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import sys
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
-
from common import find_ustht, is_processed
|
|
5
|
+
from common import find_ustht, is_processed, safe_markdown_files, safe_read_text
|
|
6
6
|
|
|
7
7
|
HELP = """Usage: python show_raw.py [--help]
|
|
8
8
|
|
|
@@ -25,13 +25,13 @@ def main():
|
|
|
25
25
|
print("No unprocessed records.")
|
|
26
26
|
return
|
|
27
27
|
|
|
28
|
-
files = [f for f in
|
|
28
|
+
files = [f for f in safe_markdown_files(ustht, raw_dir, reverse=True) if not is_processed(f, ustht)]
|
|
29
29
|
if not files:
|
|
30
30
|
print("No unprocessed records. All raw files are marked processed.")
|
|
31
31
|
return
|
|
32
32
|
|
|
33
33
|
for f in files:
|
|
34
|
-
content = f
|
|
34
|
+
content = safe_read_text(ustht, f).strip()
|
|
35
35
|
entry_count = sum(1 for line in content.splitlines() if line.strip().startswith("- ["))
|
|
36
36
|
print(f"#{f.name} ({entry_count} unprocessed entries):")
|
|
37
37
|
print(content)
|
|
@@ -5,7 +5,18 @@ from collections import defaultdict
|
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
-
from common import
|
|
8
|
+
from common import (
|
|
9
|
+
ensure_runtime_dir,
|
|
10
|
+
safe_markdown_files,
|
|
11
|
+
safe_markdown_tree,
|
|
12
|
+
safe_read_text,
|
|
13
|
+
safe_write_text,
|
|
14
|
+
find_ustht,
|
|
15
|
+
read_define_ini,
|
|
16
|
+
write_define_ini,
|
|
17
|
+
is_processed,
|
|
18
|
+
validate_dim_name,
|
|
19
|
+
)
|
|
9
20
|
|
|
10
21
|
HELP = """Usage: python sortin.py [--dry] [--help]
|
|
11
22
|
|
|
@@ -18,7 +29,7 @@ Options:
|
|
|
18
29
|
"""
|
|
19
30
|
|
|
20
31
|
|
|
21
|
-
def parse_raw_file(filepath: Path):
|
|
32
|
+
def parse_raw_file(ustht: Path, filepath: Path):
|
|
22
33
|
"""Parse raw entries from one file."""
|
|
23
34
|
entries = []
|
|
24
35
|
date = filepath.stem.split("-", 3)
|
|
@@ -27,7 +38,7 @@ def parse_raw_file(filepath: Path):
|
|
|
27
38
|
else:
|
|
28
39
|
date = datetime.now().strftime("%Y-%m-%d")
|
|
29
40
|
|
|
30
|
-
for line in filepath
|
|
41
|
+
for line in safe_read_text(ustht, filepath).splitlines():
|
|
31
42
|
line = line.strip()
|
|
32
43
|
match = re.match(r"^- \[(\d{2}:\d{2})\] (.*)$", line)
|
|
33
44
|
if not match:
|
|
@@ -51,31 +62,31 @@ def dim_path(mdbase: Path, dim: str) -> Path:
|
|
|
51
62
|
return mdbase / "details" / f"{dim}.md"
|
|
52
63
|
|
|
53
64
|
|
|
54
|
-
def count_entries(path: Path) -> int:
|
|
65
|
+
def count_entries(ustht: Path, path: Path) -> int:
|
|
55
66
|
if not path.exists():
|
|
56
67
|
return 0
|
|
57
|
-
return sum(1 for line in path
|
|
68
|
+
return sum(1 for line in safe_read_text(ustht, path).splitlines() if line.strip().startswith("- "))
|
|
58
69
|
|
|
59
70
|
|
|
60
|
-
def append_entries(path: Path, entries):
|
|
71
|
+
def append_entries(ustht: Path, path: Path, entries):
|
|
61
72
|
"""Append entries grouped by date to one dimension file."""
|
|
62
73
|
by_date = defaultdict(list)
|
|
63
74
|
for entry in entries:
|
|
64
75
|
by_date[entry["date"]].append(entry)
|
|
65
76
|
|
|
66
|
-
path.parent
|
|
77
|
+
ensure_runtime_dir(ustht, path.parent, create=True)
|
|
67
78
|
if not path.exists():
|
|
68
79
|
title = path.stem.replace("-", " ").title()
|
|
69
|
-
path
|
|
80
|
+
safe_write_text(ustht, path, f"# {title}\n\n> Project memory for `{path.stem}`.\n\n")
|
|
70
81
|
|
|
71
|
-
content = path
|
|
82
|
+
content = safe_read_text(ustht, path).rstrip()
|
|
72
83
|
for date, date_entries in sorted(by_date.items()):
|
|
73
84
|
lines = [f"- {entry['text']}" for entry in date_entries]
|
|
74
85
|
block = "\n".join(lines)
|
|
75
86
|
heading = f"## {date}"
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
87
|
+
content_lines = content.splitlines()
|
|
88
|
+
heading_idx = next((i for i, line in enumerate(content_lines) if line.strip() == heading), None)
|
|
89
|
+
if heading_idx is not None:
|
|
79
90
|
insert_idx = len(content_lines)
|
|
80
91
|
for i in range(heading_idx + 1, len(content_lines)):
|
|
81
92
|
if content_lines[i].startswith("## "):
|
|
@@ -92,31 +103,31 @@ def append_entries(path: Path, entries):
|
|
|
92
103
|
content = "\n".join(before).rstrip()
|
|
93
104
|
else:
|
|
94
105
|
content = f"{content}\n\n{heading}\n\n{block}".rstrip()
|
|
95
|
-
path
|
|
106
|
+
safe_write_text(ustht, path, content + "\n")
|
|
96
107
|
|
|
97
108
|
|
|
98
|
-
def mark_processed(filepath: Path):
|
|
109
|
+
def mark_processed(ustht: Path, filepath: Path):
|
|
99
110
|
"""Insert the processed marker at the top of a raw file."""
|
|
100
|
-
content = filepath
|
|
111
|
+
content = safe_read_text(ustht, filepath)
|
|
101
112
|
if content.split("\n", 1)[0].strip() != "<!-- processed -->":
|
|
102
|
-
filepath
|
|
113
|
+
safe_write_text(ustht, filepath, "<!-- processed -->\n" + content)
|
|
103
114
|
|
|
104
115
|
|
|
105
|
-
def update_index(mdbase: Path):
|
|
116
|
+
def update_index(ustht: Path, mdbase: Path):
|
|
106
117
|
"""Rebuild mdbase/README.ai.md with dimension counts."""
|
|
107
118
|
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
108
119
|
details = mdbase / "details"
|
|
109
120
|
dims = []
|
|
110
121
|
if details.exists():
|
|
111
|
-
dims = sorted(p.relative_to(details).with_suffix("").as_posix() for p in details
|
|
122
|
+
dims = sorted(p.relative_to(details).with_suffix("").as_posix() for p in safe_markdown_tree(ustht, details))
|
|
112
123
|
|
|
113
124
|
rows = ["| File | Dimension | Entries |", "|------|-----------|---------|"]
|
|
114
125
|
backlog = mdbase / "backlog.md"
|
|
115
126
|
if backlog.exists():
|
|
116
|
-
rows.append(f"| [backlog.md](backlog.md) | backlog | {count_entries(backlog)} |")
|
|
127
|
+
rows.append(f"| [backlog.md](backlog.md) | backlog | {count_entries(ustht, backlog)} |")
|
|
117
128
|
for dim in dims:
|
|
118
129
|
path = details / f"{dim}.md"
|
|
119
|
-
rows.append(f"| [details/{dim}.md](details/{dim}.md) | {dim} | {count_entries(path)} |")
|
|
130
|
+
rows.append(f"| [details/{dim}.md](details/{dim}.md) | {dim} | {count_entries(ustht, path)} |")
|
|
120
131
|
|
|
121
132
|
content = "\n".join([
|
|
122
133
|
"# user-thoughts mdbase Index",
|
|
@@ -137,7 +148,7 @@ def update_index(mdbase: Path):
|
|
|
137
148
|
*rows,
|
|
138
149
|
"",
|
|
139
150
|
])
|
|
140
|
-
(mdbase / "README.ai.md"
|
|
151
|
+
safe_write_text(ustht, mdbase / "README.ai.md", content)
|
|
141
152
|
|
|
142
153
|
|
|
143
154
|
def main():
|
|
@@ -161,7 +172,7 @@ def main():
|
|
|
161
172
|
print("No unprocessed records.")
|
|
162
173
|
return
|
|
163
174
|
|
|
164
|
-
raw_files = [f for f in
|
|
175
|
+
raw_files = [f for f in safe_markdown_files(ustht, raw_dir) if not is_processed(f, ustht)]
|
|
165
176
|
if not raw_files:
|
|
166
177
|
print("No unprocessed records. All raw files are marked processed.")
|
|
167
178
|
return
|
|
@@ -169,7 +180,7 @@ def main():
|
|
|
169
180
|
all_entries = []
|
|
170
181
|
entries_by_file = {}
|
|
171
182
|
for f in raw_files:
|
|
172
|
-
entries = parse_raw_file(f)
|
|
183
|
+
entries = parse_raw_file(ustht, f)
|
|
173
184
|
entries_by_file[f] = entries
|
|
174
185
|
all_entries.extend(entries)
|
|
175
186
|
|
|
@@ -194,16 +205,16 @@ def main():
|
|
|
194
205
|
return
|
|
195
206
|
|
|
196
207
|
for dim, entries in grouped.items():
|
|
197
|
-
append_entries(dim_path(mdbase, dim), entries)
|
|
208
|
+
append_entries(ustht, dim_path(mdbase, dim), entries)
|
|
198
209
|
|
|
199
210
|
for f in raw_files:
|
|
200
211
|
if entries_by_file.get(f):
|
|
201
|
-
mark_processed(f)
|
|
212
|
+
mark_processed(ustht, f)
|
|
202
213
|
|
|
203
214
|
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
204
215
|
cfg["LAST_SORTIN"] = now
|
|
205
216
|
write_define_ini(ustht, cfg)
|
|
206
|
-
update_index(mdbase)
|
|
217
|
+
update_index(ustht, mdbase)
|
|
207
218
|
print(f" LAST_SORTIN updated to {now}")
|
|
208
219
|
|
|
209
220
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import sys
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
-
from common import find_ustht, read_define_ini, is_processed
|
|
5
|
+
from common import find_ustht, read_define_ini, is_processed, safe_markdown_files, safe_markdown_tree
|
|
6
6
|
|
|
7
7
|
HELP = """Usage: python status.py [--help]
|
|
8
8
|
|
|
@@ -11,21 +11,21 @@ dimension counts.
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
def count_raw(raw_dir: Path):
|
|
14
|
+
def count_raw(ustht: Path, raw_dir: Path):
|
|
15
15
|
"""Return total and unprocessed raw file counts."""
|
|
16
16
|
if not raw_dir.exists():
|
|
17
17
|
return 0, 0
|
|
18
|
-
files =
|
|
19
|
-
unprocessed = sum(1 for f in files if not is_processed(f))
|
|
18
|
+
files = safe_markdown_files(ustht, raw_dir)
|
|
19
|
+
unprocessed = sum(1 for f in files if not is_processed(f, ustht))
|
|
20
20
|
return len(files), unprocessed
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def count_dims(mdbase: Path):
|
|
23
|
+
def count_dims(ustht: Path, mdbase: Path):
|
|
24
24
|
"""Count dimension files under mdbase/details/."""
|
|
25
25
|
details = mdbase / "details"
|
|
26
26
|
if not details.exists():
|
|
27
27
|
return 0
|
|
28
|
-
return len(
|
|
28
|
+
return len(safe_markdown_tree(ustht, details))
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def main():
|
|
@@ -42,8 +42,8 @@ def main():
|
|
|
42
42
|
skill_status = cfg.get("SKILL_STATUS", "unknown")
|
|
43
43
|
instant_status = cfg.get("INSTANT_STATUS", "unknown")
|
|
44
44
|
last_sortin = cfg.get("LAST_SORTIN", "never") or "never"
|
|
45
|
-
total_raw, unprocessed_raw = count_raw(ustht / "raw")
|
|
46
|
-
dims = count_dims(ustht / "mdbase")
|
|
45
|
+
total_raw, unprocessed_raw = count_raw(ustht, ustht / "raw")
|
|
46
|
+
dims = count_dims(ustht, ustht / "mdbase")
|
|
47
47
|
|
|
48
48
|
print(f"SKILL_STATUS={skill_status}")
|
|
49
49
|
print(f"INSTANT_STATUS={instant_status}")
|
|
@@ -3,7 +3,15 @@ import sys
|
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
-
from common import
|
|
6
|
+
from common import (
|
|
7
|
+
ensure_runtime_dir,
|
|
8
|
+
safe_markdown_files,
|
|
9
|
+
safe_read_text,
|
|
10
|
+
safe_write_text,
|
|
11
|
+
find_ustht,
|
|
12
|
+
read_define_ini,
|
|
13
|
+
validate_dim_name,
|
|
14
|
+
)
|
|
7
15
|
|
|
8
16
|
HELP = """Usage: python write_raw.py "thought text" [--dim dimension] [--help]
|
|
9
17
|
|
|
@@ -21,12 +29,14 @@ Behavior:
|
|
|
21
29
|
"""
|
|
22
30
|
|
|
23
31
|
|
|
24
|
-
def count_today_raw(raw_dir: Path) -> int:
|
|
32
|
+
def count_today_raw(ustht: Path, raw_dir: Path) -> int:
|
|
25
33
|
"""Count unprocessed entries across today's raw files."""
|
|
26
34
|
today = datetime.now().strftime("%Y-%m-%d")
|
|
27
35
|
count = 0
|
|
28
|
-
for f in
|
|
29
|
-
|
|
36
|
+
for f in safe_markdown_files(ustht, raw_dir):
|
|
37
|
+
if not f.name.startswith(today):
|
|
38
|
+
continue
|
|
39
|
+
content = safe_read_text(ustht, f)
|
|
30
40
|
first_line = content.split("\n", 1)[0].strip()
|
|
31
41
|
if first_line == "<!-- processed -->":
|
|
32
42
|
continue
|
|
@@ -72,15 +82,14 @@ def main():
|
|
|
72
82
|
print(f"Invalid dimension name: {dim}. Use lowercase letters, digits, hyphens, and optional / subdirectories.")
|
|
73
83
|
sys.exit(1)
|
|
74
84
|
|
|
75
|
-
raw_dir = ustht / "raw"
|
|
76
|
-
raw_dir.mkdir(exist_ok=True)
|
|
85
|
+
raw_dir = ensure_runtime_dir(ustht, ustht / "raw", create=True)
|
|
77
86
|
|
|
78
87
|
today = datetime.now().strftime("%Y-%m-%d")
|
|
79
88
|
now = datetime.now().strftime("%H:%M")
|
|
80
89
|
raw_file = raw_dir / f"{today}.md"
|
|
81
90
|
|
|
82
91
|
if raw_file.exists():
|
|
83
|
-
first_line = raw_file
|
|
92
|
+
first_line = safe_read_text(ustht, raw_file).split("\n", 1)[0].strip()
|
|
84
93
|
if first_line == "<!-- processed -->":
|
|
85
94
|
seq = 2
|
|
86
95
|
while (raw_dir / f"{today}-{seq}.md").exists():
|
|
@@ -92,12 +101,12 @@ def main():
|
|
|
92
101
|
entry = f"- [{now}] {thought_clean}{suffix}"
|
|
93
102
|
|
|
94
103
|
if raw_file.exists():
|
|
95
|
-
content = raw_file
|
|
96
|
-
raw_file
|
|
104
|
+
content = safe_read_text(ustht, raw_file).rstrip()
|
|
105
|
+
safe_write_text(ustht, raw_file, f"{content}\n{entry}\n")
|
|
97
106
|
else:
|
|
98
|
-
raw_file
|
|
107
|
+
safe_write_text(ustht, raw_file, f"{entry}\n")
|
|
99
108
|
|
|
100
|
-
count = count_today_raw(raw_dir)
|
|
109
|
+
count = count_today_raw(ustht, raw_dir)
|
|
101
110
|
if count > 5:
|
|
102
111
|
print(f"Today has {count} recorded thoughts. Consider running /ustht sortin.")
|
|
103
112
|
|