Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions examples/mine-all-sessions.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Mine Cursor AI, GitHub Copilot CLI, and Factory.ai sessions into the palace.

.DESCRIPTION
Runs mempalace mine against all three AI chat sources in sequence.
Each source gets its own wing so results are easy to filter.

.PARAMETER DryRun
Show what would be filed without writing to the palace.

.PARAMETER Sources
Which sources to mine. Default: all three (cursor, copilot, factory).
Pass one or more of: cursor, copilot, factory.

.EXAMPLE
.\mine-all-sessions.ps1
.\mine-all-sessions.ps1 -DryRun
.\mine-all-sessions.ps1 -Sources cursor, factory
.\mine-all-sessions.ps1 -Sources copilot -DryRun

.NOTES
Assumes `mempalace` is on PATH. Install with: pip install mempalace
or run from repo root: uv run mempalace <args>
#>

param(
[switch]$DryRun,
[ValidateSet("cursor", "copilot", "factory")]
[string[]]$Sources = @("cursor", "copilot", "factory")
)

$ErrorActionPreference = "Stop"

$CursorDir = "$env:USERPROFILE\.cursor\chats"
$CopilotDir = "$env:USERPROFILE\.copilot\session-state"
$FactoryDir = "$env:USERPROFILE\.factory\sessions"

$dryFlag = if ($DryRun) { "--dry-run" } else { "" }

function Invoke-Mine {
param([string]$Dir, [string]$Mode, [string]$Wing, [string]$Label)

if (-not (Test-Path $Dir)) {
Write-Host " [SKIP] $Label`: $Dir not found" -ForegroundColor Yellow
return
}

Write-Host ""
Write-Host "--- $Label ---" -ForegroundColor Cyan

$cmd = "python -m mempalace mine `"$Dir`" --mode $Mode --wing $Wing"
if ($DryRun) { $cmd += " --dry-run" }

Write-Host " $cmd" -ForegroundColor DarkGray
Invoke-Expression $cmd
}

Write-Host ""
Write-Host "=======================================================" -ForegroundColor Green
Write-Host " MemPalace — Mine All Sessions" -ForegroundColor Green
Write-Host "=======================================================" -ForegroundColor Green
if ($DryRun) { Write-Host " DRY RUN — nothing will be filed" -ForegroundColor Yellow }

foreach ($source in $Sources) {
switch ($source) {
"cursor" { Invoke-Mine -Dir $CursorDir -Mode "cursor" -Wing "cursor_chats" -Label "Cursor AI" }
"copilot" { Invoke-Mine -Dir $CopilotDir -Mode "convos" -Wing "copilot_sessions" -Label "GitHub Copilot CLI" }
"factory" { Invoke-Mine -Dir $FactoryDir -Mode "convos" -Wing "factory_sessions" -Label "Factory.ai" }
}
}

Write-Host ""
Write-Host "=======================================================" -ForegroundColor Green
Write-Host " Done. Search with: mempalace search `"<query>`"" -ForegroundColor Green
Write-Host "=======================================================" -ForegroundColor Green
73 changes: 73 additions & 0 deletions examples/mine-all-sessions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
# mine-all-sessions.sh — Mine Cursor, Copilot CLI, and Factory sessions into the palace.
#
# Usage:
# bash mine-all-sessions.sh # mine everything
# bash mine-all-sessions.sh --dry-run # preview only
# bash mine-all-sessions.sh copilot # mine one source only
# bash mine-all-sessions.sh cursor factory
#
# Assumes `mempalace` is on PATH (pip install mempalace, or uv run mempalace from the repo).

set -euo pipefail

DRY=""
SOURCES=()

for arg in "$@"; do
case "$arg" in
--dry-run) DRY="--dry-run" ;;
cursor|copilot|factory) SOURCES+=("$arg") ;;
*) echo "Unknown arg: $arg" >&2; exit 1 ;;
esac
done

# Default: all sources
if [[ ${#SOURCES[@]} -eq 0 ]]; then
SOURCES=(cursor copilot factory)
fi

CURSOR_DIR="${HOME}/.cursor/chats"
COPILOT_DIR="${HOME}/.copilot/session-state"
FACTORY_DIR="${HOME}/.factory/sessions"

echo ""
echo "======================================================="
echo " MemPalace — Mine All Sessions"
echo "======================================================="
[[ -n "$DRY" ]] && echo " DRY RUN — nothing will be filed"
echo ""

for source in "${SOURCES[@]}"; do
case "$source" in
cursor)
if [[ -d "$CURSOR_DIR" ]]; then
echo "--- Cursor AI ---"
mempalace mine "$CURSOR_DIR" --mode cursor --wing cursor_chats $DRY
else
echo " [SKIP] Cursor: $CURSOR_DIR not found"
fi
;;
copilot)
if [[ -d "$COPILOT_DIR" ]]; then
echo "--- GitHub Copilot CLI ---"
mempalace mine "$COPILOT_DIR" --mode convos --wing copilot_sessions $DRY
else
echo " [SKIP] Copilot: $COPILOT_DIR not found"
fi
;;
factory)
if [[ -d "$FACTORY_DIR" ]]; then
echo "--- Factory.ai ---"
mempalace mine "$FACTORY_DIR" --mode convos --wing factory_sessions $DRY
else
echo " [SKIP] Factory: $FACTORY_DIR not found"
fi
;;
esac
done

echo ""
echo "======================================================="
echo " Done. Search with: mempalace search \"<query>\""
echo "======================================================="
2 changes: 1 addition & 1 deletion hooks/mempal_save_hook.sh
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ fi

# Count human messages in the JSONL transcript
# SECURITY: Pass transcript path as sys.argv to avoid shell injection via crafted paths
if [ -f "$TRANSCRIPT_PATH" ]; then
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
EXCHANGE_COUNT=$("$MEMPAL_PYTHON_BIN" - "$TRANSCRIPT_PATH" <<'PYEOF'
import json, sys
count = 0
Expand Down
106 changes: 104 additions & 2 deletions mempalace/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,17 @@ def cmd_mine(args):
dry_run=args.dry_run,
extract_mode=args.extract,
)
elif args.mode == "cursor":
from .cursor_miner import mine_cursor

mine_cursor(
cursor_dir=args.dir,
palace_path=palace_path,
wing=args.wing or "cursor_chats",
agent=args.agent,
limit=args.limit,
dry_run=args.dry_run,
)
else:
from .miner import mine

Expand Down Expand Up @@ -260,6 +271,62 @@ def cmd_status(args):
status(palace_path=palace_path)


def cmd_kg(args):
"""Knowledge graph: add triples, query entities, show timeline."""
from datetime import date as _date
from .knowledge_graph import KnowledgeGraph

kg = KnowledgeGraph(db_path=getattr(args, "kg", None) or None)
action = args.kg_action

if action == "add":
today = str(_date.today())
kg.add_entity(args.subject)
kg.add_entity(args.obj)
triple_id = kg.add_triple(
subject=args.subject,
predicate=args.predicate,
obj=args.obj,
valid_from=args.valid_from or today,
confidence=args.confidence,
source_file=args.source or "cli",
)
print(f" + {args.subject} --[{args.predicate}]--> {args.obj}")
print(f" from: {args.valid_from or today} source: {args.source or 'cli'} id: {triple_id[:8]}")

elif action == "query":
results = kg.query_entity(args.entity, as_of=args.as_of, direction=args.direction)
if not results:
print(f" No facts found for: {args.entity}")
return
print(f"\n Facts for: {args.entity} (as_of={args.as_of or 'now'})")
print(" " + "-" * 50)
for r in results:
valid = f"{r.get('valid_from', '?')} -> {r.get('valid_to') or 'present'}"
src = r.get("source_file", "")
print(f" [{r['direction']}] {r['subject']} --[{r['predicate']}]--> {r['object']}")
print(f" {valid} | {src}")

elif action == "timeline":
results = kg.timeline(entity_name=args.entity)
if not results:
entity_label = args.entity or "all entities"
print(f" No timeline found for: {entity_label}")
return
entity_label = args.entity or "all entities"
print(f"\n Timeline: {entity_label}")
print(" " + "-" * 50)
for r in results:
ended = f" [ended {r['valid_to']}]" if r.get("valid_to") else ""
print(f" {r.get('valid_from', '?')} {r['subject']} --[{r['predicate']}]--> {r['object']}{ended}")

elif action == "stats":
s = kg.stats()
print(f"\n KG stats: {s.get('entities', 0)} entities, {s.get('triples', 0)} triples")
for pred, count in (s.get("predicates") or {}).items():
print(f" {pred}: {count}")


def cmd_repair(args):
"""Rebuild palace vector index from SQLite metadata."""
import shutil
Expand Down Expand Up @@ -557,9 +624,9 @@ def main():
p_mine.add_argument("dir", help="Directory to mine")
p_mine.add_argument(
"--mode",
choices=["projects", "convos"],
choices=["projects", "convos", "cursor"],
default="projects",
help="Ingest mode: 'projects' for code/docs (default), 'convos' for chat exports",
help="Ingest mode: 'projects' for code/docs (default), 'convos' for chat exports, 'cursor' for Cursor AI chats (~/.cursor/chats)",
)
p_mine.add_argument("--wing", default=None, help="Wing name (default: directory name)")
p_mine.add_argument(
Expand Down Expand Up @@ -589,6 +656,34 @@ def main():
help="Extraction strategy for convos mode: 'exchange' (default) or 'general' (5 memory types)",
)

# kg — knowledge graph
p_kg = sub.add_parser("kg", help="Knowledge graph: add triples, query entities, timeline")
p_kg.add_argument(
"--kg",
default=None,
metavar="PATH",
help="Path to knowledge_graph.sqlite3 (default: ~/.mempalace/knowledge_graph.sqlite3)",
)
kg_sub = p_kg.add_subparsers(dest="kg_action")

p_kg_add = kg_sub.add_parser("add", help="Add a triple: subject predicate object")
p_kg_add.add_argument("subject", help="Subject entity (e.g. petition053)")
p_kg_add.add_argument("predicate", help="Relationship (e.g. has_status, touches, references_adr)")
p_kg_add.add_argument("obj", help="Object entity (e.g. implemented, opendebt-debt-service)")
p_kg_add.add_argument("--source", default=None, help="Agent or tool that asserted this fact")
p_kg_add.add_argument("--from", dest="valid_from", default=None, help="Valid from date (default: today)")
p_kg_add.add_argument("--confidence", type=float, default=1.0, help="Confidence 0-1 (default: 1.0)")

p_kg_query = kg_sub.add_parser("query", help="Query an entity's relationships")
p_kg_query.add_argument("entity", help="Entity name to query")
p_kg_query.add_argument("--as-of", default=None, help="Point-in-time query (ISO date)")
p_kg_query.add_argument("--direction", choices=["in", "out", "both"], default="both")

p_kg_timeline = kg_sub.add_parser("timeline", help="Show temporal timeline for an entity")
p_kg_timeline.add_argument("entity", nargs="?", default=None, help="Entity name (default: all)")

kg_sub.add_parser("stats", help="Show KG statistics")

# sweep
p_sweep = sub.add_parser(
"sweep",
Expand Down Expand Up @@ -711,6 +806,13 @@ def main():
return

# Handle two-level subcommands
if args.command == "kg":
if not getattr(args, "kg_action", None):
p_kg.print_help()
return
cmd_kg(args)
return

if args.command == "hook":
if not getattr(args, "hook_action", None):
p_hook.print_help()
Expand Down
4 changes: 2 additions & 2 deletions mempalace/convo_miner.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,9 +451,9 @@ def mine_convos(

type_counts = Counter(c.get("memory_type", "general") for c in chunks)
types_str = ", ".join(f"{t}:{n}" for t, n in type_counts.most_common())
print(f" [DRY RUN] {filepath.name} {len(chunks)} memories ({types_str})")
print(f" [DRY RUN] {filepath.name} -> {len(chunks)} memories ({types_str})")
else:
print(f" [DRY RUN] {filepath.name} room:{room} ({len(chunks)} drawers)")
print(f" [DRY RUN] {filepath.name} -> room:{room} ({len(chunks)} drawers)")
total_drawers += len(chunks)
# Track room counts
if extract_mode == "general":
Expand Down
Loading