Skip to content

Instantly share code, notes, and snippets.

@sifbuilder
Created January 21, 2026 20:59
Show Gist options
  • Select an option

  • Save sifbuilder/d3e3cd709dd7d6385f2f3ab1cb037023 to your computer and use it in GitHub Desktop.

Select an option

Save sifbuilder/d3e3cd709dd7d6385f2f3ab1cb037023 to your computer and use it in GitHub Desktop.
thinker - CLI thinking partner
#!/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