@demig0d2/skills 1.0.0

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.
Files changed (29) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/bin/cli.js +313 -0
  4. package/package.json +44 -0
  5. package/skills/book-writer/SKILL.md +1396 -0
  6. package/skills/book-writer/references/kdp_specs.md +139 -0
  7. package/skills/book-writer/scripts/kdp_check.py +255 -0
  8. package/skills/book-writer/scripts/toc_extract.py +151 -0
  9. package/skills/book-writer/scripts/word_count.py +196 -0
  10. package/skills/chapter-auditor/SKILL.md +231 -0
  11. package/skills/chapter-auditor/scripts/score_report.py +237 -0
  12. package/skills/concept-expander/SKILL.md +170 -0
  13. package/skills/concept-expander/scripts/validate_concept.py +255 -0
  14. package/skills/continuity-tracker/SKILL.md +251 -0
  15. package/skills/continuity-tracker/references/log_schema.md +149 -0
  16. package/skills/continuity-tracker/scripts/conflict_check.py +179 -0
  17. package/skills/continuity-tracker/scripts/log_manager.py +258 -0
  18. package/skills/humanizer/SKILL.md +632 -0
  19. package/skills/humanizer/references/patterns_quick_ref.md +71 -0
  20. package/skills/humanizer/scripts/dna_scan.py +168 -0
  21. package/skills/humanizer/scripts/scan_ai_patterns.py +279 -0
  22. package/skills/overhaul/SKILL.md +697 -0
  23. package/skills/overhaul/references/upgrade_checklist.md +81 -0
  24. package/skills/overhaul/scripts/changelog_gen.py +183 -0
  25. package/skills/overhaul/scripts/skill_parser.py +265 -0
  26. package/skills/overhaul/scripts/version_bump.py +128 -0
  27. package/skills/research-aggregator/SKILL.md +194 -0
  28. package/skills/research-aggregator/references/thinkers_reference.md +104 -0
  29. package/skills/research-aggregator/scripts/bank_formatter.py +206 -0
@@ -0,0 +1,149 @@
1
+ # Continuity Log — Schema Reference
2
+
3
+ The continuity log is a JSON file (`continuity_log.json`) maintained
4
+ alongside the manuscript. This document describes every field.
5
+
6
+ ---
7
+
8
+ ## Full Schema
9
+
10
+ ```json
11
+ {
12
+ "book_title": "string — the book's working title",
13
+ "created": "ISO datetime — when the log was initialized",
14
+ "last_updated": "ISO datetime — last update timestamp",
15
+ "last_chapter": "integer — last chapter number completed",
16
+
17
+ "commitments": [
18
+ "string — book-level promises established in front matter/intro"
19
+ ],
20
+
21
+ "established_facts": [
22
+ "string — factual details about the author's story, must stay consistent"
23
+ ],
24
+
25
+ "metaphors": {
26
+ "by_chapter": {
27
+ "1": ["metaphor used in chapter 1", "another metaphor"],
28
+ "2": ["metaphor used in chapter 2"]
29
+ },
30
+ "available": ["strong images not yet used"],
31
+ "retired": ["all metaphors used — do not repeat"]
32
+ },
33
+
34
+ "insights": {
35
+ "1": "Core insight fully delivered in chapter 1",
36
+ "2": "Core insight delivered in chapter 2"
37
+ },
38
+
39
+ "tone_decisions": {
40
+ "register": "string — e.g., intimate/confessional in My Story",
41
+ "reader_address": "string — e.g., direct 'you' in Reflection",
42
+ "author_position": "string — e.g., always in-process, never above",
43
+ "emotional_ceiling": "string — e.g., darkest content in Ch.1"
44
+ },
45
+
46
+ "structural_patterns": {
47
+ "section_format": "string — e.g., My Story + My Reflection",
48
+ "avg_chapter_words": "integer — running average",
49
+ "opening_style": "string — e.g., immersive scene",
50
+ "closing_style": "string — e.g., single reframe sentence",
51
+ "chapter_word_counts": [2400, 2200, 2600]
52
+ },
53
+
54
+ "open_threads": [
55
+ {
56
+ "thread": "string — the thread description",
57
+ "introduced_in": "string — chapter number",
58
+ "closed": false,
59
+ "added": "ISO datetime"
60
+ }
61
+ ],
62
+
63
+ "closed_threads": [
64
+ {
65
+ "thread": "string",
66
+ "introduced_in": "string",
67
+ "closed": true,
68
+ "closed_in": "string — chapter where resolved",
69
+ "added": "ISO datetime"
70
+ }
71
+ ],
72
+
73
+ "chapter_summaries": {
74
+ "1": {
75
+ "title": "Chapter 1 title",
76
+ "summary": "One-paragraph summary of what was covered"
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Field Guidelines
85
+
86
+ ### `commitments`
87
+ Book-level promises that must never be contradicted. Examples:
88
+ - "Author frames himself as someone who lived this, not an expert"
89
+ - "Book promises no quick fixes — lasting transformation only"
90
+ - "Author is always in-process, never looking back from arrival"
91
+
92
+ ### `established_facts`
93
+ All narrative details stated in the manuscript that must remain consistent:
94
+ - "Father passed away during author's degree years"
95
+ - "Lived in a tiny penthouse apartment alone"
96
+ - "Walked miles to save money on transport"
97
+ - "Cooked rice and lentils, stretched groceries"
98
+
99
+ ### `metaphors.retired`
100
+ Once a metaphor is used anywhere in the manuscript, it goes here.
101
+ The conflict checker (`conflict_check.py`) scans new chapters against this list.
102
+
103
+ ### `insights`
104
+ One core insight per chapter. Track to avoid restating:
105
+ - Ch.1: "Loneliness as mirror of self-relationship"
106
+ - Ch.2: "Chasing connection from fear, not love, creates neediness"
107
+ - Ch.3: "Aloneness and loneliness are different — one is chosen"
108
+
109
+ ### `open_threads`
110
+ Thematic or narrative threads introduced that haven't been resolved:
111
+ - "Ch.2 mentioned 'the small proof that my life has value' — needs deeper exploration"
112
+ - "Ch.3 established reader's 'wrong attempts' pattern — needs to pay off in Ch.7"
113
+
114
+ ---
115
+
116
+ ## CLI Quick Reference
117
+
118
+ ```bash
119
+ # Initialize
120
+ python log_manager.py init "My Book Title"
121
+
122
+ # Add chapter
123
+ python log_manager.py add-chapter 1 "Why It Hurts So Much"
124
+
125
+ # Add established fact
126
+ python log_manager.py add-fact "Author's father passed during degree years"
127
+
128
+ # Add insight delivered
129
+ python log_manager.py add-insight 1 "Loneliness as mirror of self-relationship"
130
+
131
+ # Add metaphor (auto-retires it)
132
+ python log_manager.py add-metaphor 1 "deafening scream inside my head"
133
+
134
+ # Add open thread
135
+ python log_manager.py add-thread "Ch.2 mentioned small proof of self-value — needs depth"
136
+
137
+ # Close a thread (by index)
138
+ python log_manager.py threads # list open threads
139
+ python log_manager.py close-thread 0 # close thread at index 0
140
+
141
+ # View full log
142
+ python log_manager.py show
143
+
144
+ # Summary only
145
+ python log_manager.py summary
146
+
147
+ # Conflict check before finalizing a chapter
148
+ python conflict_check.py chapter_5.md --log continuity_log.json
149
+ ```
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ conflict_check.py — Check a chapter for continuity conflicts against the log
4
+
5
+ Scans a chapter text file against the established facts, retired metaphors,
6
+ and delivered insights in the continuity log. Surfaces potential conflicts.
7
+
8
+ Usage:
9
+ python conflict_check.py <chapter_file> [--log log.json]
10
+ python conflict_check.py <chapter_file> [--log log.json] --json
11
+ """
12
+
13
+ import sys
14
+ import re
15
+ import json
16
+ import argparse
17
+ from pathlib import Path
18
+
19
+ DEFAULT_LOG = "continuity_log.json"
20
+
21
+ # ─── Conflict Detection Rules ─────────────────────────────────────────────────
22
+
23
+ # Known fact patterns that might be stated inconsistently
24
+ NUMERICAL_PATTERN = re.compile(r'\b(\d+)\s+(years?|months?|days?|chapters?|books?|pages?)\b', re.IGNORECASE)
25
+ NAME_PATTERN = re.compile(r'\b[A-Z][a-z]+ [A-Z][a-z]+\b')
26
+
27
+
28
+ def load_log(log_path: str) -> dict:
29
+ path = Path(log_path)
30
+ if not path.exists():
31
+ return None
32
+ return json.loads(path.read_text(encoding="utf-8"))
33
+
34
+
35
+ def check_retired_metaphors(chapter_text: str, retired: list) -> list:
36
+ """Find retired metaphors used again in this chapter."""
37
+ conflicts = []
38
+ chapter_lower = chapter_text.lower()
39
+ for metaphor in retired:
40
+ # Check key phrases from the metaphor
41
+ key_words = [w for w in metaphor.lower().split() if len(w) > 4]
42
+ matches = sum(1 for w in key_words if w in chapter_lower)
43
+ if matches >= 2:
44
+ conflicts.append({
45
+ "type": "repeated_metaphor",
46
+ "severity": "medium",
47
+ "detail": f'Retired metaphor may be reused: "{metaphor}"',
48
+ "suggestion": "Use a fresh image — this metaphor was already delivered in a prior chapter",
49
+ })
50
+ return conflicts
51
+
52
+
53
+ def check_repeated_insights(chapter_text: str, insights: dict) -> list:
54
+ """Check if an insight already fully delivered is being re-delivered."""
55
+ conflicts = []
56
+ chapter_lower = chapter_text.lower()
57
+
58
+ for ch_num, insight in insights.items():
59
+ # Look for key phrase overlap
60
+ insight_words = [w for w in insight.lower().split() if len(w) > 5]
61
+ if not insight_words:
62
+ continue
63
+ matches = sum(1 for w in insight_words if w in chapter_lower)
64
+ overlap_ratio = matches / len(insight_words) if insight_words else 0
65
+
66
+ if overlap_ratio > 0.5:
67
+ conflicts.append({
68
+ "type": "repeated_insight",
69
+ "severity": "medium",
70
+ "detail": f'Insight from Ch.{ch_num} may be re-delivered: "{insight[:60]}..."',
71
+ "suggestion": "Build on this insight rather than restating it — add a new dimension",
72
+ })
73
+
74
+ return conflicts
75
+
76
+
77
+ def check_fact_consistency(chapter_text: str, established_facts: list) -> list:
78
+ """Flag when numerical or named facts appear inconsistently."""
79
+ conflicts = []
80
+
81
+ # Extract numbers from chapter
82
+ chapter_numbers = NUMERICAL_PATTERN.findall(chapter_text)
83
+
84
+ for fact in established_facts:
85
+ # Check if fact contains numbers that contradict chapter
86
+ fact_numbers = NUMERICAL_PATTERN.findall(fact)
87
+ for fn_val, fn_unit in fact_numbers:
88
+ for cn_val, cn_unit in chapter_numbers:
89
+ if cn_unit.rstrip("s") == fn_unit.rstrip("s") and cn_val != fn_val:
90
+ conflicts.append({
91
+ "type": "numerical_inconsistency",
92
+ "severity": "high",
93
+ "detail": f'Number mismatch: fact says "{fn_val} {fn_unit}", chapter says "{cn_val} {cn_unit}"',
94
+ "suggestion": f'Verify against established fact: "{fact[:80]}"',
95
+ })
96
+
97
+ return conflicts
98
+
99
+
100
+ def run_conflict_check(chapter_path: str, log_path: str) -> dict:
101
+ chapter_text = Path(chapter_path).read_text(encoding="utf-8")
102
+ log = load_log(log_path)
103
+
104
+ result = {
105
+ "chapter_file": chapter_path,
106
+ "log_file": log_path,
107
+ "conflicts": [],
108
+ "warnings": [],
109
+ "clean": True,
110
+ }
111
+
112
+ if not log:
113
+ result["warnings"].append("No continuity log found — run log_manager.py init first")
114
+ return result
115
+
116
+ # Run checks
117
+ retired = log.get("metaphors", {}).get("retired", [])
118
+ insights = log.get("insights", {})
119
+ facts = log.get("established_facts", [])
120
+
121
+ metaphor_conflicts = check_retired_metaphors(chapter_text, retired)
122
+ insight_conflicts = check_repeated_insights(chapter_text, insights)
123
+ fact_conflicts = check_fact_consistency(chapter_text, facts)
124
+
125
+ all_conflicts = metaphor_conflicts + insight_conflicts + fact_conflicts
126
+
127
+ result["conflicts"] = all_conflicts
128
+ result["clean"] = len(all_conflicts) == 0
129
+
130
+ return result
131
+
132
+
133
+ def print_report(result: dict):
134
+ print(f"\n{'═' * 60}")
135
+ print(f" CONTINUITY CONFLICT CHECK")
136
+ print(f" Chapter: {result['chapter_file']}")
137
+ print(f" Log: {result['log_file']}")
138
+ print(f"{'═' * 60}")
139
+
140
+ if result.get("warnings"):
141
+ for w in result["warnings"]:
142
+ print(f"\n ⚠ {w}")
143
+
144
+ if result["clean"]:
145
+ print(f"\n ✓ No conflicts detected. Chapter is consistent with log.\n")
146
+ return
147
+
148
+ high = [c for c in result["conflicts"] if c["severity"] == "high"]
149
+ medium = [c for c in result["conflicts"] if c["severity"] == "medium"]
150
+
151
+ print(f"\n Conflicts: {len(result['conflicts'])} ({len(high)} high, {len(medium)} medium)\n")
152
+
153
+ for i, conflict in enumerate(result["conflicts"], 1):
154
+ icon = "✗" if conflict["severity"] == "high" else "⚠"
155
+ print(f" {icon} [{conflict['severity'].upper()}] {conflict['type'].replace('_', ' ').title()}")
156
+ print(f" {conflict['detail']}")
157
+ print(f" → {conflict['suggestion']}\n")
158
+
159
+ print(f"{'═' * 60}")
160
+ print(f" Resolve conflicts before updating the continuity log.\n")
161
+
162
+
163
+ if __name__ == "__main__":
164
+ parser = argparse.ArgumentParser()
165
+ parser.add_argument("chapter_file")
166
+ parser.add_argument("--log", default=DEFAULT_LOG)
167
+ parser.add_argument("--json", action="store_true")
168
+ args = parser.parse_args()
169
+
170
+ if not Path(args.chapter_file).exists():
171
+ print(f"Error: File not found: {args.chapter_file}", file=sys.stderr)
172
+ sys.exit(1)
173
+
174
+ result = run_conflict_check(args.chapter_file, args.log)
175
+
176
+ if args.json:
177
+ print(json.dumps(result, indent=2))
178
+ else:
179
+ print_report(result)
@@ -0,0 +1,258 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ log_manager.py — Read, write, and update the continuity log
4
+
5
+ The continuity log is stored as a JSON file alongside the manuscript.
6
+ This script manages all log operations: init, update, read, and summary.
7
+
8
+ Usage:
9
+ python log_manager.py init <book_title> [--log log.json]
10
+ python log_manager.py show [--log log.json]
11
+ python log_manager.py summary [--log log.json]
12
+ python log_manager.py add-chapter <N> <title> [--log log.json]
13
+ python log_manager.py add-fact <fact> [--log log.json]
14
+ python log_manager.py add-insight <chapter_n> <insight> [--log log.json]
15
+ python log_manager.py add-metaphor <chapter_n> <metaphor> [--log log.json]
16
+ python log_manager.py add-thread <thread> [--log log.json]
17
+ python log_manager.py close-thread <thread_index> [--log log.json]
18
+ python log_manager.py threads [--log log.json]
19
+ """
20
+
21
+ import sys
22
+ import json
23
+ import argparse
24
+ from pathlib import Path
25
+ from datetime import datetime
26
+
27
+ DEFAULT_LOG = "continuity_log.json"
28
+
29
+
30
+ def load_log(log_path: str) -> dict:
31
+ path = Path(log_path)
32
+ if path.exists():
33
+ return json.loads(path.read_text(encoding="utf-8"))
34
+ return None
35
+
36
+
37
+ def save_log(log: dict, log_path: str):
38
+ Path(log_path).write_text(json.dumps(log, indent=2, ensure_ascii=False), encoding="utf-8")
39
+
40
+
41
+ def init_log(book_title: str, log_path: str) -> dict:
42
+ log = {
43
+ "book_title": book_title,
44
+ "created": datetime.now().isoformat(),
45
+ "last_updated": datetime.now().isoformat(),
46
+ "last_chapter": 0,
47
+ "commitments": [],
48
+ "established_facts": [],
49
+ "metaphors": {
50
+ "by_chapter": {},
51
+ "available": [],
52
+ "retired": [],
53
+ },
54
+ "insights": {},
55
+ "tone_decisions": {
56
+ "register": "",
57
+ "reader_address": "",
58
+ "author_position": "",
59
+ "emotional_ceiling": "",
60
+ },
61
+ "structural_patterns": {
62
+ "section_format": "",
63
+ "avg_chapter_words": 0,
64
+ "opening_style": "",
65
+ "closing_style": "",
66
+ "chapter_word_counts": [],
67
+ },
68
+ "open_threads": [],
69
+ "closed_threads": [],
70
+ "chapter_summaries": {},
71
+ }
72
+ save_log(log, log_path)
73
+ print(f" ✓ Log initialized: {log_path}")
74
+ print(f" Book: {book_title}")
75
+ return log
76
+
77
+
78
+ def show_log(log: dict):
79
+ print(f"\n{'═' * 60}")
80
+ print(f" CONTINUITY LOG — {log['book_title']}")
81
+ print(f" Last updated after Chapter {log['last_chapter']}")
82
+ print(f"{'═' * 60}")
83
+
84
+ if log["commitments"]:
85
+ print(f"\n BOOK-LEVEL COMMITMENTS ({len(log['commitments'])}):")
86
+ for c in log["commitments"]:
87
+ print(f" • {c}")
88
+
89
+ if log["established_facts"]:
90
+ print(f"\n ESTABLISHED FACTS ({len(log['established_facts'])}):")
91
+ for f in log["established_facts"]:
92
+ print(f" • {f}")
93
+
94
+ if log["metaphors"]["by_chapter"]:
95
+ print(f"\n METAPHORS USED:")
96
+ for ch, mets in sorted(log["metaphors"]["by_chapter"].items(), key=lambda x: int(x[0])):
97
+ print(f" Ch.{ch}: {', '.join(mets)}")
98
+ if log["metaphors"]["retired"]:
99
+ print(f" Retired: {', '.join(log['metaphors']['retired'])}")
100
+
101
+ if log["insights"]:
102
+ print(f"\n INSIGHTS DELIVERED:")
103
+ for ch, insight in sorted(log["insights"].items(), key=lambda x: int(x[0])):
104
+ print(f" Ch.{ch}: {insight}")
105
+
106
+ open_threads = [t for t in log["open_threads"] if not t.get("closed")]
107
+ if open_threads:
108
+ print(f"\n OPEN THREADS ({len(open_threads)}):")
109
+ for i, t in enumerate(open_threads):
110
+ print(f" [{i}] {t['thread']}")
111
+ if t.get("introduced_in"):
112
+ print(f" Introduced: Ch.{t['introduced_in']}")
113
+
114
+ if log["chapter_summaries"]:
115
+ print(f"\n CHAPTER SUMMARIES ({len(log['chapter_summaries'])}):")
116
+ for ch, summary in sorted(log["chapter_summaries"].items(), key=lambda x: int(x[0])):
117
+ title = summary.get("title", f"Chapter {ch}")
118
+ text = summary.get("summary", "")[:80]
119
+ print(f" Ch.{ch} — {title}: {text}...")
120
+
121
+ print(f"\n{'═' * 60}\n")
122
+
123
+
124
+ def summary_log(log: dict):
125
+ open_threads = [t for t in log["open_threads"] if not t.get("closed")]
126
+ print(f"\n {log['book_title']} — Continuity Summary")
127
+ print(f" Chapters completed: {log['last_chapter']}")
128
+ print(f" Facts established: {len(log['established_facts'])}")
129
+ print(f" Insights delivered: {len(log['insights'])}")
130
+ print(f" Open threads: {len(open_threads)}")
131
+ if open_threads:
132
+ print(f" ⚠ Unresolved threads:")
133
+ for t in open_threads:
134
+ print(f" · {t['thread'][:60]}")
135
+ print()
136
+
137
+
138
+ if __name__ == "__main__":
139
+ parser = argparse.ArgumentParser()
140
+ parser.add_argument("command", choices=[
141
+ "init", "show", "summary", "add-chapter", "add-fact",
142
+ "add-insight", "add-metaphor", "add-thread", "close-thread", "threads"
143
+ ])
144
+ parser.add_argument("args", nargs="*")
145
+ parser.add_argument("--log", default=DEFAULT_LOG)
146
+ parsed = parser.parse_args()
147
+
148
+ log_path = parsed.log
149
+ cmd = parsed.command
150
+ args = parsed.args
151
+
152
+ if cmd == "init":
153
+ title = " ".join(args) if args else "Untitled"
154
+ init_log(title, log_path)
155
+
156
+ elif cmd in ("show",):
157
+ log = load_log(log_path)
158
+ if not log:
159
+ print(f" No log found at {log_path}. Run: python log_manager.py init <title>")
160
+ sys.exit(1)
161
+ show_log(log)
162
+
163
+ elif cmd == "summary":
164
+ log = load_log(log_path)
165
+ if not log:
166
+ print(f" No log found at {log_path}")
167
+ sys.exit(1)
168
+ summary_log(log)
169
+
170
+ elif cmd == "add-chapter":
171
+ log = load_log(log_path)
172
+ if not log:
173
+ print(" No log found. Run init first.")
174
+ sys.exit(1)
175
+ n = int(args[0]) if args else log["last_chapter"] + 1
176
+ title = " ".join(args[1:]) if len(args) > 1 else f"Chapter {n}"
177
+ log["last_chapter"] = n
178
+ log["last_updated"] = datetime.now().isoformat()
179
+ if str(n) not in log["chapter_summaries"]:
180
+ log["chapter_summaries"][str(n)] = {"title": title, "summary": ""}
181
+ save_log(log, log_path)
182
+ print(f" ✓ Chapter {n}: {title} added to log")
183
+
184
+ elif cmd == "add-fact":
185
+ log = load_log(log_path)
186
+ if not log:
187
+ sys.exit(1)
188
+ fact = " ".join(args)
189
+ log["established_facts"].append(fact)
190
+ log["last_updated"] = datetime.now().isoformat()
191
+ save_log(log, log_path)
192
+ print(f" ✓ Fact added: {fact}")
193
+
194
+ elif cmd == "add-insight":
195
+ log = load_log(log_path)
196
+ if not log:
197
+ sys.exit(1)
198
+ ch = args[0] if args else str(log["last_chapter"])
199
+ insight = " ".join(args[1:]) if len(args) > 1 else ""
200
+ log["insights"][ch] = insight
201
+ log["last_updated"] = datetime.now().isoformat()
202
+ save_log(log, log_path)
203
+ print(f" ✓ Insight for Ch.{ch} added")
204
+
205
+ elif cmd == "add-metaphor":
206
+ log = load_log(log_path)
207
+ if not log:
208
+ sys.exit(1)
209
+ ch = args[0] if args else str(log["last_chapter"])
210
+ metaphor = " ".join(args[1:]) if len(args) > 1 else ""
211
+ if ch not in log["metaphors"]["by_chapter"]:
212
+ log["metaphors"]["by_chapter"][ch] = []
213
+ log["metaphors"]["by_chapter"][ch].append(metaphor)
214
+ log["metaphors"]["retired"].append(metaphor)
215
+ log["last_updated"] = datetime.now().isoformat()
216
+ save_log(log, log_path)
217
+ print(f" ✓ Metaphor added to Ch.{ch} and retired")
218
+
219
+ elif cmd == "add-thread":
220
+ log = load_log(log_path)
221
+ if not log:
222
+ sys.exit(1)
223
+ thread = " ".join(args)
224
+ log["open_threads"].append({
225
+ "thread": thread,
226
+ "introduced_in": str(log["last_chapter"]),
227
+ "closed": False,
228
+ "added": datetime.now().isoformat(),
229
+ })
230
+ log["last_updated"] = datetime.now().isoformat()
231
+ save_log(log, log_path)
232
+ print(f" ✓ Thread added: {thread}")
233
+
234
+ elif cmd == "close-thread":
235
+ log = load_log(log_path)
236
+ if not log:
237
+ sys.exit(1)
238
+ idx = int(args[0]) if args else -1
239
+ open_threads = [t for t in log["open_threads"] if not t.get("closed")]
240
+ if 0 <= idx < len(open_threads):
241
+ open_threads[idx]["closed"] = True
242
+ open_threads[idx]["closed_in"] = str(log["last_chapter"])
243
+ log["closed_threads"].append(open_threads[idx])
244
+ log["last_updated"] = datetime.now().isoformat()
245
+ save_log(log, log_path)
246
+ print(f" ✓ Thread [{idx}] closed: {open_threads[idx]['thread'][:60]}")
247
+ else:
248
+ print(f" ✗ Invalid thread index: {idx}")
249
+
250
+ elif cmd == "threads":
251
+ log = load_log(log_path)
252
+ if not log:
253
+ sys.exit(1)
254
+ open_threads = [t for t in log["open_threads"] if not t.get("closed")]
255
+ print(f"\n Open threads ({len(open_threads)}):\n")
256
+ for i, t in enumerate(open_threads):
257
+ print(f" [{i}] Introduced Ch.{t.get('introduced_in', '?')}: {t['thread']}")
258
+ print()