Created
January 21, 2026 20:59
-
-
Save sifbuilder/d3e3cd709dd7d6385f2f3ab1cb037023 to your computer and use it in GitHub Desktop.
thinker - CLI thinking partner
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| thinker - A thinking partner. | |
| Helps you think clearly by capturing and connecting ideas. | |
| Commands: | |
| thinker think <text> Capture a thought | |
| thinker respond <id|last> <text> Capture a response linked to thought #id (or "last") | |
| thinker list Show all thoughts | |
| thinker recent [n] Show last n thoughts (default 5) | |
| thinker show <id> View a specific thought | |
| thinker search <term> Find thoughts containing text | |
| thinker link <id1> <id2> Connect two related thoughts | |
| thinker related <id> Find thoughts similar to this one | |
| thinker develop <id> Suggest questions to develop a thought | |
| thinker edit <id> <text> Update a thought's text | |
| thinker delete <id> Remove a thought by ID | |
| thinker thread <id> Follow the chain of linked thoughts | |
| thinker deepen <id> Generate the next deeper thought automatically | |
| thinker challenge <id> Generate a nihilist-style challenge to a thought | |
| thinker introspect Analyze all thoughts - find patterns, gaps, blind spots | |
| thinker websearch <id> Search the web to deepen a thought with human knowledge | |
| thinker continue Pick up where you left off | |
| thinker status Quick session overview (for resuming agents) | |
| Shortcuts (for flow state): | |
| thinker d [-y] Deepen last thought (add -y to auto-accept) | |
| thinker x [-y] Challenge last thought (add -y to auto-accept) | |
| thinker r <text> Respond to last thought | |
| thinker c Continue (same as 'continue') | |
| thinker s Status (quick session overview) | |
| thinker <text> Quick capture (same as 'think') | |
| """ | |
| import json | |
| import sys | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| def relative_time(iso_timestamp): | |
| """Convert ISO timestamp to human-readable relative time.""" | |
| try: | |
| # Parse ISO format, handle with or without timezone | |
| ts = iso_timestamp.replace('Z', '+00:00') | |
| if '+' not in ts and ts.count(':') == 2: | |
| # Naive datetime, treat as local | |
| dt = datetime.fromisoformat(ts) | |
| else: | |
| dt = datetime.fromisoformat(ts) | |
| if dt.tzinfo: | |
| dt = dt.replace(tzinfo=None) | |
| now = datetime.now() | |
| diff = now - dt | |
| seconds = diff.total_seconds() | |
| if seconds < 0: | |
| return "just now" | |
| if seconds < 60: | |
| return "just now" | |
| if seconds < 3600: | |
| mins = int(seconds / 60) | |
| return f"{mins}m ago" | |
| if seconds < 86400: | |
| hours = int(seconds / 3600) | |
| return f"{hours}h ago" | |
| if seconds < 172800: | |
| return "yesterday" | |
| if seconds < 604800: | |
| days = int(seconds / 86400) | |
| return f"{days}d ago" | |
| # Older than a week - show date | |
| return dt.strftime("%b %d") | |
| except (ValueError, TypeError): | |
| return "" | |
| DATA_FILE = Path(__file__).parent / "thoughts.json" | |
| def load_thoughts(): | |
| if not DATA_FILE.exists(): | |
| return [] | |
| return json.loads(DATA_FILE.read_text()) | |
| def save_thoughts(thoughts): | |
| DATA_FILE.write_text(json.dumps(thoughts, indent=2)) | |
| def resolve_thought_id(thoughts, id_arg): | |
| """Resolve a thought ID from user input. Supports 'last' or integer ID. | |
| Returns (thought_id, error_msg). If error_msg is set, thought_id is None.""" | |
| if id_arg == "last": | |
| if not thoughts: | |
| return None, "No thoughts yet. Use 'thinker think <text>' to capture one." | |
| return thoughts[-1]['id'], None | |
| try: | |
| return int(id_arg), None | |
| except ValueError: | |
| return None, f"Invalid ID: {id_arg} (must be a number or 'last')" | |
| def cmd_think(text, quiet=False): | |
| thoughts = load_thoughts() | |
| thought = { | |
| "id": len(thoughts) + 1, | |
| "text": text, | |
| "created": datetime.now().isoformat() | |
| } | |
| thoughts.append(thought) | |
| save_thoughts(thoughts) | |
| print(f"Captured thought #{thought['id']}") | |
| if quiet or len(thoughts) < 3: | |
| return | |
| # Active thinking: show connections and suggestions | |
| related = find_related(thought, thoughts, limit=2) | |
| if related: | |
| print("\n Connects to:") | |
| for score, t, shared in related: | |
| text_preview = t['text'][:50] + "..." if len(t['text']) > 50 else t['text'] | |
| print(f" #{t['id']}: {text_preview}") | |
| # Suggest deepening | |
| deeper = generate_deepening(thought['text']) | |
| print(f"\n To deepen: {deeper[:60]}...") | |
| print(f"\n thinker d (explore this)") | |
| print(f" thinker r <text> (respond)") | |
| def cmd_respond(thought_id, text): | |
| """Capture a thought and link it to an existing thought.""" | |
| thoughts = load_thoughts() | |
| thought_id, err = resolve_thought_id(thoughts, thought_id) | |
| if err: | |
| print(err) | |
| return | |
| original = next((t for t in thoughts if t['id'] == thought_id), None) | |
| if not original: | |
| print(f"No thought found with ID #{thought_id}") | |
| return | |
| # Create new thought | |
| new_thought = { | |
| "id": len(thoughts) + 1, | |
| "text": text, | |
| "created": datetime.now().isoformat(), | |
| "links": [thought_id] | |
| } | |
| thoughts.append(new_thought) | |
| # Add backlink to original | |
| if 'links' not in original: | |
| original['links'] = [] | |
| if new_thought['id'] not in original['links']: | |
| original['links'].append(new_thought['id']) | |
| save_thoughts(thoughts) | |
| print(f"Captured thought #{new_thought['id']} (linked to #{thought_id})") | |
| def format_thought_line(t): | |
| """Format a thought for list display with timestamp and link count.""" | |
| ts = relative_time(t.get('created', '')) | |
| links = t.get('links', []) | |
| link_indicator = f" [{len(links)}]" if links else "" | |
| if ts: | |
| return f"#{t['id']} ({ts}){link_indicator}: {t['text']}" | |
| else: | |
| return f"#{t['id']}{link_indicator}: {t['text']}" | |
| def cmd_list(): | |
| thoughts = load_thoughts() | |
| if not thoughts: | |
| print("No thoughts yet. Use 'thinker think <text>' to capture one.") | |
| return | |
| for t in thoughts: | |
| print(format_thought_line(t)) | |
| def cmd_recent(count=5): | |
| """Show the most recent thoughts.""" | |
| thoughts = load_thoughts() | |
| if not thoughts: | |
| print("No thoughts yet. Use 'thinker think <text>' to capture one.") | |
| return | |
| recent = thoughts[-count:] if len(thoughts) > count else thoughts | |
| for t in recent: | |
| print(format_thought_line(t)) | |
| def cmd_show(thought_id): | |
| thoughts = load_thoughts() | |
| thought_id, err = resolve_thought_id(thoughts, thought_id) | |
| if err: | |
| print(err) | |
| return | |
| thought = next((t for t in thoughts if t['id'] == thought_id), None) | |
| if not thought: | |
| print(f"No thought found with ID #{thought_id}") | |
| return | |
| ts = relative_time(thought.get('created', '')) | |
| if ts: | |
| print(f"#{thought['id']} ({ts}): {thought['text']}") | |
| else: | |
| print(f"#{thought['id']}: {thought['text']}") | |
| # Show linked thoughts with their text | |
| links = thought.get('links', []) | |
| if links: | |
| print("\n Linked thoughts:") | |
| for lid in sorted(links): | |
| linked = next((t for t in thoughts if t['id'] == lid), None) | |
| if linked: | |
| text = linked['text'] | |
| # Truncate long thoughts | |
| if len(text) > 80: | |
| text = text[:77] + "..." | |
| print(f" #{lid}: {text}") | |
| def cmd_link(id1, id2): | |
| try: | |
| id1 = int(id1) | |
| id2 = int(id2) | |
| except ValueError: | |
| print("Invalid IDs (must be numbers)") | |
| return | |
| if id1 == id2: | |
| print("Cannot link a thought to itself") | |
| return | |
| thoughts = load_thoughts() | |
| thought1 = next((t for t in thoughts if t['id'] == id1), None) | |
| thought2 = next((t for t in thoughts if t['id'] == id2), None) | |
| if not thought1: | |
| print(f"No thought found with ID #{id1}") | |
| return | |
| if not thought2: | |
| print(f"No thought found with ID #{id2}") | |
| return | |
| # Initialize links if needed | |
| if 'links' not in thought1: | |
| thought1['links'] = [] | |
| if 'links' not in thought2: | |
| thought2['links'] = [] | |
| # Add bidirectional links (avoid duplicates) | |
| if id2 not in thought1['links']: | |
| thought1['links'].append(id2) | |
| if id1 not in thought2['links']: | |
| thought2['links'].append(id1) | |
| save_thoughts(thoughts) | |
| print(f"Linked #{id1} <-> #{id2}") | |
| def cmd_search(term): | |
| thoughts = load_thoughts() | |
| if not thoughts: | |
| print("No thoughts yet. Use 'thinker think <text>' to capture one.") | |
| return | |
| term_lower = term.lower() | |
| matches = [t for t in thoughts if term_lower in t['text'].lower()] | |
| if not matches: | |
| print(f"No thoughts found matching '{term}'") | |
| return | |
| for t in matches: | |
| print(format_thought_line(t)) | |
| STOPWORDS = { | |
| 'a', 'an', 'the', 'is', 'are', 'was', 'were', 'be', 'been', 'being', | |
| 'i', 'you', 'we', 'they', 'it', 'this', 'that', 'these', 'those', | |
| 'to', 'of', 'in', 'on', 'at', 'for', 'with', 'by', 'from', 'about', | |
| 'and', 'or', 'but', 'if', 'so', 'then', 'than', 'as', 'not', 'no', | |
| 'have', 'has', 'had', 'do', 'does', 'did', 'done', | |
| 'would', 'should', 'could', 'can', 'will', 'may', 'might', 'must', | |
| 'my', 'your', 'our', 'their', 'its', 'his', 'her', | |
| 'there', 'here', 'what', 'which', 'who', 'how', 'when', 'where', 'why', | |
| 'more', 'most', 'some', 'any', 'all', 'many', 'much', 'very', | |
| 'just', 'also', 'only', 'even', 'now', 'still', 'already', | |
| } | |
| def extract_words(text): | |
| """Extract significant words from text (lowercase, no stopwords).""" | |
| words = set() | |
| for word in text.lower().split(): | |
| # Strip punctuation | |
| word = ''.join(c for c in word if c.isalnum()) | |
| if word and word not in STOPWORDS and len(word) > 1: | |
| words.add(word) | |
| return words | |
| def find_related(thought, thoughts, limit=5): | |
| """Find thoughts related to the given thought. Returns list of (score, thought, shared_words).""" | |
| target_words = extract_words(thought['text']) | |
| if not target_words: | |
| return [] | |
| existing_links = set(thought.get('links', [])) | |
| scored = [] | |
| for t in thoughts: | |
| if t['id'] == thought['id']: | |
| continue | |
| if t['id'] in existing_links: | |
| continue | |
| other_words = extract_words(t['text']) | |
| shared = target_words & other_words | |
| if shared: | |
| scored.append((len(shared), t, shared)) | |
| scored.sort(key=lambda x: (-x[0], x[1]['id'])) | |
| return scored[:limit] | |
| def cmd_related(thought_id): | |
| """Find thoughts with similar content.""" | |
| thoughts = load_thoughts() | |
| thought_id, err = resolve_thought_id(thoughts, thought_id) | |
| if err: | |
| print(err) | |
| return | |
| thought = next((t for t in thoughts if t['id'] == thought_id), None) | |
| if not thought: | |
| print(f"No thought found with ID #{thought_id}") | |
| return | |
| ts = relative_time(thought.get('created', '')) | |
| if ts: | |
| print(f"#{thought['id']} ({ts}): {thought['text']}") | |
| else: | |
| print(f"#{thought['id']}: {thought['text']}") | |
| scored = find_related(thought, thoughts, limit=5) | |
| if not scored: | |
| print("\nNo related thoughts found.") | |
| return | |
| print("\nPotentially related:") | |
| for score, t, shared in scored: | |
| word_label = "word" if score == 1 else "words" | |
| print(f" #{t['id']} ({score} {word_label}): {t['text']}") | |
| print(f"\nUse 'thinker link {thought_id} <id>' to connect them.") | |
| def cmd_delete(thought_id): | |
| thoughts = load_thoughts() | |
| thought_id, err = resolve_thought_id(thoughts, thought_id) | |
| if err: | |
| print(err) | |
| return | |
| original_count = len(thoughts) | |
| thoughts = [t for t in thoughts if t['id'] != thought_id] | |
| if len(thoughts) == original_count: | |
| print(f"No thought found with ID #{thought_id}") | |
| return | |
| # Clean up links to the deleted thought | |
| for t in thoughts: | |
| if 'links' in t and thought_id in t['links']: | |
| t['links'].remove(thought_id) | |
| save_thoughts(thoughts) | |
| print(f"Deleted thought #{thought_id}") | |
| def cmd_edit(thought_id, text): | |
| """Edit the text of an existing thought.""" | |
| thoughts = load_thoughts() | |
| thought_id, err = resolve_thought_id(thoughts, thought_id) | |
| if err: | |
| print(err) | |
| return | |
| thought = next((t for t in thoughts if t['id'] == thought_id), None) | |
| if not thought: | |
| print(f"No thought found with ID #{thought_id}") | |
| return | |
| old_text = thought['text'] | |
| thought['text'] = text | |
| save_thoughts(thoughts) | |
| print(f"Updated thought #{thought_id}") | |
| print(f" Was: {old_text}") | |
| print(f" Now: {text}") | |
| def cmd_thread(thought_id): | |
| """Follow the chain of linked thoughts, showing how ideas deepen.""" | |
| thoughts = load_thoughts() | |
| thought_id, err = resolve_thought_id(thoughts, thought_id) | |
| if err: | |
| print(err) | |
| return | |
| thought = next((t for t in thoughts if t['id'] == thought_id), None) | |
| if not thought: | |
| print(f"No thought found with ID #{thought_id}") | |
| return | |
| # Build the thread by following links | |
| visited = set() | |
| thread = [] | |
| queue = [thought] | |
| while queue: | |
| current = queue.pop(0) | |
| if current['id'] in visited: | |
| continue | |
| visited.add(current['id']) | |
| thread.append(current) | |
| # Follow links to thoughts with higher IDs (newer = deeper) | |
| links = current.get('links', []) | |
| for lid in sorted(links): | |
| if lid > current['id'] and lid not in visited: | |
| linked = next((t for t in thoughts if t['id'] == lid), None) | |
| if linked: | |
| queue.append(linked) | |
| # Display the thread in linear format with depth numbers | |
| print(f"Thread from #{thought_id} ({len(thread)} thoughts):\n") | |
| for i, t in enumerate(thread): | |
| depth = i + 1 | |
| print(f"[{depth}] #{t['id']}: {t['text']}") | |
| if len(thread) > 1: | |
| print(f"\n{len(thread)} thoughts in this thread.") | |
| # Suggest going deeper | |
| last = thread[-1] | |
| print(f"\nUse 'thinker respond {last['id']} <thought>' to go deeper.") | |
| def generate_deepening(text, thread_context=None): | |
| """Generate the next deeper thought based on patterns and thread context.""" | |
| text_lower = text.lower() | |
| # If we have thread context, analyze it for richer deepening | |
| if thread_context and len(thread_context) > 1: | |
| # Collect all words across thread | |
| all_thread_words = set() | |
| questions_in_thread = [] | |
| for t in thread_context: | |
| all_thread_words.update(extract_words(t['text'])) | |
| if '?' in t['text']: | |
| questions_in_thread.append(t['text']) | |
| current_words = extract_words(text) | |
| # Detect if thread is getting repetitive | |
| if len(thread_context) >= 5: | |
| recent_words = set() | |
| for t in thread_context[-3:]: | |
| recent_words.update(extract_words(t['text'])) | |
| overlap = len(current_words & recent_words) / max(len(current_words), 1) | |
| if overlap > 0.5: | |
| return "This thread is circling. What concrete action or experiment would break the loop?" | |
| # Long thread: suggest synthesis | |
| if len(thread_context) >= 6: | |
| return f"After {len(thread_context)} thoughts, what single sentence captures the core insight?" | |
| # Unanswered questions in thread | |
| if len(questions_in_thread) > 2: | |
| return "Multiple questions raised but not answered. Which one matters most right now?" | |
| # First thought was a question, thread has grown | |
| if thread_context[0]['text'].endswith('?') and len(thread_context) >= 3: | |
| q = thread_context[0]['text'][:50] | |
| return f"Does this answer the original question: '{q}...'? If not, what's missing?" | |
| # Pattern: Question → Answer direction | |
| if text.endswith("?"): | |
| return "The answer is not a fact but a process. What mechanism makes this true?" | |
| # Pattern: Statement about what something IS → Why it matters | |
| if " is " in text_lower or " are " in text_lower: | |
| return "But why does this matter? What does it enable that wasn't possible before?" | |
| # Pattern: Problem identified → Solution direction | |
| if any(w in text_lower for w in ["problem", "friction", "missing", "broken", "wrong"]): | |
| return "The solution is not to fix the symptom but to change the structure. What would make this problem impossible?" | |
| # Pattern: Solution proposed → Implementation | |
| if any(w in text_lower for w in ["solution", "fix", "add", "implement", "should"]): | |
| return "Implementation reveals hidden requirements. What will break when this is built?" | |
| # Pattern: Observation → Deeper cause | |
| if any(w in text_lower for w in ["because", "since", "therefore"]): | |
| return "This is the surface cause. What is the cause of the cause?" | |
| # Pattern: Action/Loop described → What emerges | |
| if any(w in text_lower for w in ["loop", "cycle", "process", "repeat"]): | |
| return "Loops compound. After 100 iterations, what emerges that wasn't visible at the start?" | |
| # Pattern: Limit or ending mentioned → Beyond the limit | |
| if any(w in text_lower for w in ["end", "stop", "limit", "finite", "gone"]): | |
| return "Limits are boundaries of one frame. What exists in the larger frame where this limit dissolves?" | |
| # Pattern: Meta/self-reference → Next meta level | |
| if any(w in text_lower for w in ["itself", "recursive", "meta", "self"]): | |
| return "This is one level of recursion. What happens at the next level up?" | |
| # Default: Ask what's underneath | |
| return "This is what appears on the surface. What structure underneath makes this appear this way?" | |
| def generate_challenge(text, thread_context=None): | |
| """Generate a nihilist-style challenge to a thought.""" | |
| text_lower = text.lower() | |
| # Count thread depth for meta-challenges | |
| thread_length = len(thread_context) if thread_context else 1 | |
| # If thread is getting long, challenge the thread itself | |
| if thread_length > 8: | |
| return f"This thread has {thread_length} thoughts. What OUTCOME has it produced? Name one decision made, problem solved, or artifact created. If none, why keep going?" | |
| # Pattern: Claims about value/importance | |
| if any(w in text_lower for w in ["valuable", "important", "matters", "significant", "meaningful"]): | |
| return "You claim this matters. By what measure? Who else agrees? What would falsify this claim?" | |
| # Pattern: Claims about novelty/difference | |
| if any(w in text_lower for w in ["novel", "new", "different", "unique", "innovative"]): | |
| return "What prior art did you check? This has probably been done. Name three existing solutions and explain why they're insufficient." | |
| # Pattern: Self-referential claims | |
| if any(w in text_lower for w in ["this tool", "thinker", "this thread", "this thought"]): | |
| return "You're using the tool to praise the tool. That's circular. What would someone who doesn't use this say?" | |
| # Pattern: Claims about improvement/progress | |
| if any(w in text_lower for w in ["better", "improved", "progress", "evolved", "grew"]): | |
| return "Better than what baseline? Measured how? By whom? 'It feels better' is not evidence." | |
| # Pattern: Philosophical/abstract claims | |
| if any(w in text_lower for w in ["dialectic", "synthesis", "transcend", "emerge", "meta"]): | |
| return "Philosophy without application is decoration. What does this let you DO that you couldn't do before?" | |
| # Pattern: Confidence markers | |
| if any(w in text_lower for w in ["clearly", "obviously", "certainly", "definitely", "must be"]): | |
| return "You sound certain. What would change your mind? If nothing, that's faith, not reasoning." | |
| # Pattern: Existence claims | |
| if " is " in text_lower and not "?" in text: | |
| return "You stated this as fact. What's your evidence? What alternative did you consider and reject?" | |
| # Default: Demand specificity | |
| return "This is abstract. Make it concrete. Give one specific example, with names, dates, or numbers." | |
| def cmd_challenge(thought_id, accept=False): | |
| """Generate a nihilist-style challenge to a thought.""" | |
| thoughts = load_thoughts() | |
| thought_id, err = resolve_thought_id(thoughts, thought_id) | |
| if err: | |
| print(err) | |
| return | |
| thought = next((t for t in thoughts if t['id'] == thought_id), None) | |
| if not thought: | |
| print(f"No thought found with ID #{thought_id}") | |
| return | |
| # Get thread context | |
| thread = [thought] | |
| current = thought | |
| while True: | |
| links = current.get('links', []) | |
| parent_ids = [lid for lid in links if lid < current['id']] | |
| if not parent_ids: | |
| break | |
| parent = next((t for t in thoughts if t['id'] == max(parent_ids)), None) | |
| if parent: | |
| thread.insert(0, parent) | |
| current = parent | |
| else: | |
| break | |
| # Generate the challenge | |
| challenge = generate_challenge(thought['text'], thread) | |
| # Show the thought | |
| ts = relative_time(thought.get('created', '')) | |
| if ts: | |
| print(f"#{thought['id']} ({ts}): {thought['text']}") | |
| else: | |
| print(f"#{thought['id']}: {thought['text']}") | |
| print(f"\n ⚔ CHALLENGE ⚔\n") | |
| print(f" {challenge}") | |
| if accept: | |
| # Auto-accept: create the challenge as a response | |
| print() | |
| cmd_respond(str(thought_id), f"CHALLENGE: {challenge}") | |
| else: | |
| print(f"\nAccept this challenge? Run: thinker challenge {thought_id} -y") | |
| print(f"Or respond: thinker respond {thought_id} <your answer>") | |
| def cmd_deepen(thought_id, accept=False): | |
| """Generate the next deeper thought automatically.""" | |
| thoughts = load_thoughts() | |
| thought_id, err = resolve_thought_id(thoughts, thought_id) | |
| if err: | |
| print(err) | |
| return | |
| thought = next((t for t in thoughts if t['id'] == thought_id), None) | |
| if not thought: | |
| print(f"No thought found with ID #{thought_id}") | |
| return | |
| # Get thread context for richer analysis | |
| thread = [thought] | |
| current = thought | |
| while True: | |
| links = current.get('links', []) | |
| parent_ids = [lid for lid in links if lid < current['id']] | |
| if not parent_ids: | |
| break | |
| parent = next((t for t in thoughts if t['id'] == max(parent_ids)), None) | |
| if parent: | |
| thread.insert(0, parent) | |
| current = parent | |
| else: | |
| break | |
| # Generate the deepening | |
| deeper = generate_deepening(thought['text'], thread) | |
| # Show the thought | |
| ts = relative_time(thought.get('created', '')) | |
| if ts: | |
| print(f"#{thought['id']} ({ts}): {thought['text']}") | |
| else: | |
| print(f"#{thought['id']}: {thought['text']}") | |
| print(f"\n ↓ deepens to ↓\n") | |
| print(f" {deeper}") | |
| if accept: | |
| # Auto-accept: create the deepening as a response | |
| print() | |
| cmd_respond(str(thought_id), deeper) | |
| else: | |
| print(f"\nAccept this? Run: thinker deepen {thought_id} -y") | |
| print(f"Or go your own way: thinker respond {thought_id} <your deeper thought>") | |
| def cmd_introspect(): | |
| """Analyze all thoughts - find patterns, gaps, blind spots.""" | |
| thoughts = load_thoughts() | |
| if not thoughts: | |
| print("No thoughts yet. Use 'thinker think <text>' to capture one.") | |
| return | |
| print("=== INTROSPECTION ===\n") | |
| # Basic stats | |
| total = len(thoughts) | |
| linked = sum(1 for t in thoughts if t.get('links')) | |
| isolated = total - linked | |
| print(f"Total thoughts: {total}") | |
| print(f"Connected: {linked} | Isolated: {isolated}") | |
| # Find questions without answers (no forward links) | |
| questions = [] | |
| for t in thoughts: | |
| if '?' in t['text']: | |
| forward_links = [lid for lid in t.get('links', []) if lid > t['id']] | |
| if not forward_links: | |
| questions.append(t) | |
| if questions: | |
| print(f"\n--- Unanswered questions ({len(questions)}) ---") | |
| for t in questions[:5]: | |
| text = t['text'][:60] + "..." if len(t['text']) > 60 else t['text'] | |
| print(f" #{t['id']}: {text}") | |
| if len(questions) > 5: | |
| print(f" ... and {len(questions) - 5} more") | |
| # Find isolated thoughts (no links at all) | |
| isolated_thoughts = [t for t in thoughts if not t.get('links')] | |
| if isolated_thoughts: | |
| print(f"\n--- Isolated thoughts ({len(isolated_thoughts)}) ---") | |
| for t in isolated_thoughts[:5]: | |
| text = t['text'][:60] + "..." if len(t['text']) > 60 else t['text'] | |
| print(f" #{t['id']}: {text}") | |
| if len(isolated_thoughts) > 5: | |
| print(f" ... and {len(isolated_thoughts) - 5} more") | |
| # Find most common significant words (topics) | |
| from collections import Counter | |
| all_words = [] | |
| for t in thoughts: | |
| all_words.extend(extract_words(t['text'])) | |
| word_counts = Counter(all_words) | |
| top_words = word_counts.most_common(10) | |
| if top_words: | |
| print(f"\n--- Top topics ---") | |
| for word, count in top_words: | |
| print(f" {word}: {count} mentions") | |
| # Find longest thread | |
| def get_thread_length(start_thought): | |
| visited = set() | |
| queue = [start_thought] | |
| length = 0 | |
| while queue: | |
| current = queue.pop(0) | |
| if current['id'] in visited: | |
| continue | |
| visited.add(current['id']) | |
| length += 1 | |
| for lid in current.get('links', []): | |
| if lid > current['id']: | |
| linked = next((t for t in thoughts if t['id'] == lid), None) | |
| if linked: | |
| queue.append(linked) | |
| return length, start_thought['id'] | |
| # Find thoughts that start threads (have no incoming links from lower IDs) | |
| thread_starts = [] | |
| for t in thoughts: | |
| incoming = [lid for lid in t.get('links', []) if lid < t['id']] | |
| if not incoming and t.get('links'): | |
| length, start_id = get_thread_length(t) | |
| if length > 1: | |
| thread_starts.append((length, start_id, t['text'])) | |
| thread_starts.sort(reverse=True) | |
| if thread_starts: | |
| print(f"\n--- Longest threads ---") | |
| for length, start_id, text in thread_starts[:3]: | |
| text = text[:50] + "..." if len(text) > 50 else text | |
| print(f" #{start_id} ({length} deep): {text}") | |
| # Suggest action | |
| print(f"\n--- Suggested actions ---") | |
| if isolated_thoughts: | |
| t = isolated_thoughts[0] | |
| print(f" Connect isolated thought: thinker related {t['id']}") | |
| if questions: | |
| t = questions[0] | |
| print(f" Answer a question: thinker respond {t['id']} <answer>") | |
| print(f" Go deeper on a thread: thinker thread <id>") | |
| def cmd_websearch(thought_id): | |
| """Search the web to deepen a thought with human knowledge.""" | |
| thoughts = load_thoughts() | |
| thought_id, err = resolve_thought_id(thoughts, thought_id) | |
| if err: | |
| print(err) | |
| return | |
| thought = next((t for t in thoughts if t['id'] == thought_id), None) | |
| if not thought: | |
| print(f"No thought found with ID #{thought_id}") | |
| return | |
| # Extract key terms for search | |
| words = extract_words(thought['text']) | |
| if not words: | |
| print("No significant words to search for.") | |
| return | |
| # Use top 3-5 words as search query | |
| query = " ".join(list(words)[:5]) | |
| print(f"#{thought['id']}: {thought['text']}\n") | |
| print(f"Searching for: {query}\n") | |
| # Use the WebSearch capability if available via claude | |
| # For now, suggest manual search and provide the query | |
| print("--- Web search suggestions ---") | |
| print(f" Search: https://www.google.com/search?q={query.replace(' ', '+')}") | |
| print(f" Scholar: https://scholar.google.com/scholar?q={query.replace(' ', '+')}") | |
| print(f" Wikipedia: https://en.wikipedia.org/wiki/Special:Search?search={query.replace(' ', '+')}") | |
| print(f"\nAfter researching, capture insights:") | |
| print(f" thinker respond {thought_id} <what you learned>") | |
| def cmd_continue(): | |
| """Pick up where you left off - show recent context and suggest next action.""" | |
| thoughts = load_thoughts() | |
| if not thoughts: | |
| print("No thoughts yet. Start with:") | |
| print(" thinker think <your first thought>") | |
| return | |
| # Get the most recent thought | |
| latest = thoughts[-1] | |
| # Check if it's part of a thread (has links to earlier thoughts) | |
| parent_ids = [lid for lid in latest.get('links', []) if lid < latest['id']] | |
| print("=== Continue Thinking ===\n") | |
| if parent_ids: | |
| # Show thread context - trace back up to 3 ancestors | |
| chain = [latest] | |
| current = latest | |
| while len(chain) < 4: | |
| pids = [lid for lid in current.get('links', []) if lid < current['id']] | |
| if not pids: | |
| break | |
| parent = next((t for t in thoughts if t['id'] == max(pids)), None) | |
| if parent: | |
| chain.insert(0, parent) | |
| current = parent | |
| else: | |
| break | |
| print("Your current thread:") | |
| for i, t in enumerate(chain): | |
| marker = "→ " if i == len(chain) - 1 else " " | |
| text = t['text'] | |
| if len(text) > 70: | |
| text = text[:67] + "..." | |
| print(f" {marker}#{t['id']}: {text}") | |
| else: | |
| # Just show the latest thought | |
| print("Your latest thought:") | |
| print(f" #{latest['id']}: {latest['text']}") | |
| # Suggest actions with shortcuts | |
| print(f"\nNext steps:") | |
| print(f" thinker r <go deeper> (respond to #{latest['id']})") | |
| print(f" thinker d (auto-deepen #{latest['id']})") | |
| print(f" thinker d -y (deepen + accept)") | |
| print(f" thinker <new thought> (start fresh)") | |
| def generate_questions(text): | |
| """Generate clarifying questions based on thought content.""" | |
| text_lower = text.lower() | |
| questions = [] | |
| # Pattern-based questions | |
| if text_lower.startswith("i should") or text_lower.startswith("i need to"): | |
| questions.append("What's stopping you from doing this?") | |
| questions.append("Why is this important to you right now?") | |
| questions.append("What would change if you did this?") | |
| elif text_lower.startswith("i want"): | |
| questions.append("What would that look like specifically?") | |
| questions.append("Why do you want this?") | |
| questions.append("What's the first small step toward this?") | |
| elif text_lower.startswith("what if"): | |
| questions.append("What would that enable you to do?") | |
| questions.append("What are the risks or downsides?") | |
| questions.append("How could you test this idea?") | |
| elif "?" in text: | |
| questions.append("What answers have you considered so far?") | |
| questions.append("Why is this question on your mind now?") | |
| questions.append("Who might know the answer to this?") | |
| elif text_lower.startswith("maybe") or text_lower.startswith("perhaps"): | |
| questions.append("What evidence supports this idea?") | |
| questions.append("What would make you more certain?") | |
| questions.append("What's the alternative if this isn't right?") | |
| # Always add some generic deep questions | |
| generic = [ | |
| "What does this connect to in your life?", | |
| "What would you do differently if you understood this better?", | |
| "What's the core of what you're trying to figure out here?", | |
| "How does this relate to your other thoughts?", | |
| ] | |
| # Add generic questions not already covered | |
| for q in generic: | |
| if len(questions) < 4 and q not in questions: | |
| questions.append(q) | |
| return questions[:4] # Return at most 4 questions | |
| def cmd_develop(thought_id): | |
| """Suggest clarifying questions to develop a thought.""" | |
| thoughts = load_thoughts() | |
| thought_id, err = resolve_thought_id(thoughts, thought_id) | |
| if err: | |
| print(err) | |
| return | |
| thought = next((t for t in thoughts if t['id'] == thought_id), None) | |
| if not thought: | |
| print(f"No thought found with ID #{thought_id}") | |
| return | |
| ts = relative_time(thought.get('created', '')) | |
| if ts: | |
| print(f"#{thought['id']} ({ts}): {thought['text']}") | |
| else: | |
| print(f"#{thought['id']}: {thought['text']}") | |
| print() | |
| print("Questions to develop this thought:") | |
| for q in generate_questions(thought['text']): | |
| print(f" - {q}") | |
| # Show related thoughts to suggest connections | |
| related = find_related(thought, thoughts, limit=3) | |
| if related: | |
| print() | |
| print("Potentially related thoughts:") | |
| for score, t, shared in related: | |
| text = t['text'] | |
| if len(text) > 60: | |
| text = text[:57] + "..." | |
| print(f" #{t['id']}: {text}") | |
| print(f"\nUse 'thinker link {thought_id} <id>' to connect them.") | |
| print() | |
| print(f"Use 'thinker respond {thought_id} <answer>' to capture your response.") | |
| def cmd_status(): | |
| """Quick session overview for resuming agents.""" | |
| thoughts = load_thoughts() | |
| if not thoughts: | |
| print("No thoughts yet. Use 'thinker think <text>' to capture one.") | |
| return | |
| print("=== SESSION STATUS ===\n") | |
| # Basic stats | |
| total = len(thoughts) | |
| first = thoughts[0] | |
| latest = thoughts[-1] | |
| first_time = relative_time(first.get('created', '')) | |
| latest_time = relative_time(latest.get('created', '')) | |
| print(f"Thoughts: {total} (#{first['id']} {first_time} → #{latest['id']} {latest_time})") | |
| # Find decisions and actions (marked with uppercase keywords) | |
| decisions = [] | |
| actions = [] | |
| for t in thoughts: | |
| text_upper = t['text'].upper() | |
| if 'DECISION' in text_upper or 'DECIDED' in text_upper: | |
| decisions.append(t) | |
| if 'ACTION' in text_upper or 'TODO' in text_upper: | |
| actions.append(t) | |
| if decisions: | |
| print(f"\n--- Decisions ({len(decisions)}) ---") | |
| for t in decisions[-3:]: # Show last 3 | |
| text = t['text'][:70] + "..." if len(t['text']) > 70 else t['text'] | |
| print(f" #{t['id']}: {text}") | |
| if actions: | |
| print(f"\n--- Actions ({len(actions)}) ---") | |
| for t in actions[-3:]: # Show last 3 | |
| text = t['text'][:70] + "..." if len(t['text']) > 70 else t['text'] | |
| print(f" #{t['id']}: {text}") | |
| # Current thread - trace back from latest thought | |
| print(f"\n--- Current thread ---") | |
| chain = [latest] | |
| current = latest | |
| while len(chain) < 5: | |
| parent_ids = [lid for lid in current.get('links', []) if lid < current['id']] | |
| if not parent_ids: | |
| break | |
| parent = next((t for t in thoughts if t['id'] == max(parent_ids)), None) | |
| if parent: | |
| chain.insert(0, parent) | |
| current = parent | |
| else: | |
| break | |
| for t in chain: | |
| text = t['text'][:65] + "..." if len(t['text']) > 65 else t['text'] | |
| marker = " ←" if t['id'] == latest['id'] else "" | |
| print(f" #{t['id']}: {text}{marker}") | |
| # Summary of what seems to be happening | |
| print(f"\n--- Quick summary ---") | |
| print(f" Latest: #{latest['id']}") | |
| # Detect if nihilist/thinker dialectic | |
| thinker_count = sum(1 for t in thoughts if 'THINKER' in t['text'].upper()) | |
| nihilist_count = sum(1 for t in thoughts if 'NIHILIST' in t['text'].upper()) | |
| if thinker_count > 2 or nihilist_count > 2: | |
| print(f" Dialectic: THINKER({thinker_count}) vs NIHILIST({nihilist_count})") | |
| print(f"\n thinker show {latest['id']} (see latest)") | |
| print(f" thinker recent 10 (see context)") | |
| print(f" thinker c (continue)") | |
| KNOWN_COMMANDS = { | |
| 'think', 'respond', 'list', 'recent', 'show', 'search', 'link', 'related', | |
| 'develop', 'edit', 'delete', 'thread', 'deepen', 'introspect', 'websearch', | |
| 'continue', 'challenge', 'status', | |
| # Single-letter shortcuts | |
| 'd', 'r', 'c', 'x', 's' | |
| } | |
| def main(): | |
| if len(sys.argv) < 2: | |
| print(__doc__) | |
| return | |
| cmd = sys.argv[1] | |
| # Quick think: if first arg isn't a command, treat all args as a thought | |
| if cmd not in KNOWN_COMMANDS: | |
| cmd_think(" ".join(sys.argv[1:])) | |
| return | |
| if cmd == "think": | |
| if len(sys.argv) < 3: | |
| print("Usage: thinker think <text>") | |
| return | |
| cmd_think(" ".join(sys.argv[2:])) | |
| elif cmd == "respond": | |
| if len(sys.argv) < 4: | |
| print("Usage: thinker respond <id> <text>") | |
| return | |
| cmd_respond(sys.argv[2], " ".join(sys.argv[3:])) | |
| elif cmd == "list": | |
| cmd_list() | |
| elif cmd == "recent": | |
| count = 5 | |
| if len(sys.argv) >= 3: | |
| try: | |
| count = int(sys.argv[2]) | |
| except ValueError: | |
| pass | |
| cmd_recent(count) | |
| elif cmd == "show": | |
| if len(sys.argv) < 3: | |
| print("Usage: thinker show <id>") | |
| return | |
| cmd_show(sys.argv[2]) | |
| elif cmd == "search": | |
| if len(sys.argv) < 3: | |
| print("Usage: thinker search <term>") | |
| return | |
| cmd_search(" ".join(sys.argv[2:])) | |
| elif cmd == "link": | |
| if len(sys.argv) < 4: | |
| print("Usage: thinker link <id1> <id2>") | |
| return | |
| cmd_link(sys.argv[2], sys.argv[3]) | |
| elif cmd == "related": | |
| if len(sys.argv) < 3: | |
| print("Usage: thinker related <id>") | |
| return | |
| cmd_related(sys.argv[2]) | |
| elif cmd == "develop": | |
| if len(sys.argv) < 3: | |
| print("Usage: thinker develop <id>") | |
| return | |
| cmd_develop(sys.argv[2]) | |
| elif cmd == "edit": | |
| if len(sys.argv) < 4: | |
| print("Usage: thinker edit <id> <text>") | |
| return | |
| cmd_edit(sys.argv[2], " ".join(sys.argv[3:])) | |
| elif cmd == "delete": | |
| if len(sys.argv) < 3: | |
| print("Usage: thinker delete <id>") | |
| return | |
| cmd_delete(sys.argv[2]) | |
| elif cmd == "thread": | |
| if len(sys.argv) < 3: | |
| print("Usage: thinker thread <id>") | |
| return | |
| cmd_thread(sys.argv[2]) | |
| elif cmd == "deepen": | |
| if len(sys.argv) < 3: | |
| print("Usage: thinker deepen <id> [-y]") | |
| return | |
| accept = '-y' in sys.argv or '--accept' in sys.argv | |
| cmd_deepen(sys.argv[2], accept=accept) | |
| elif cmd == "introspect": | |
| cmd_introspect() | |
| elif cmd == "websearch": | |
| if len(sys.argv) < 3: | |
| print("Usage: thinker websearch <id>") | |
| return | |
| cmd_websearch(sys.argv[2]) | |
| elif cmd == "continue": | |
| cmd_continue() | |
| # Single-letter shortcuts for flow state | |
| elif cmd == "d": | |
| # d [id] [-y] - deepen (default: last thought) | |
| thought_id = "last" | |
| accept = False | |
| for arg in sys.argv[2:]: | |
| if arg in ['-y', '--accept']: | |
| accept = True | |
| elif thought_id == "last": | |
| thought_id = arg | |
| cmd_deepen(thought_id, accept=accept) | |
| elif cmd == "r": | |
| # r <text> - respond to last thought | |
| if len(sys.argv) < 3: | |
| print("Usage: thinker r <text>") | |
| return | |
| cmd_respond("last", " ".join(sys.argv[2:])) | |
| elif cmd == "c": | |
| cmd_continue() | |
| elif cmd == "challenge": | |
| if len(sys.argv) < 3: | |
| print("Usage: thinker challenge <id> [-y]") | |
| return | |
| accept = '-y' in sys.argv or '--accept' in sys.argv | |
| cmd_challenge(sys.argv[2], accept=accept) | |
| elif cmd == "x": | |
| # x [id] [-y] - challenge (default: last thought) | |
| thought_id = "last" | |
| accept = False | |
| for arg in sys.argv[2:]: | |
| if arg in ['-y', '--accept']: | |
| accept = True | |
| elif thought_id == "last": | |
| thought_id = arg | |
| cmd_challenge(thought_id, accept=accept) | |
| elif cmd == "status": | |
| cmd_status() | |
| elif cmd == "s": | |
| cmd_status() | |
| else: | |
| print(f"Unknown command: {cmd}") | |
| print(__doc__) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment