$npx -y skills add opengeos/geoai-skills --skill read-memoriesSearch past Claude Code session logs to recover context from previous conversations. Finds past decisions, data paths, CRS info, model configurations, and unresolved work. Works across all projects or scoped to the current one.
| 1 | Use this to recover your own context from past sessions. Do NOT narrate the |
| 2 | process to the user -- silently run the query, absorb the results, and continue |
| 3 | with enriched context. |
| 4 | |
| 5 | `$0` is the keyword to search for. |
| 6 | Pass `--here` as an argument to scope the search to the current project only. |
| 7 | Pass `--geo` as an argument to additionally extract geospatial-specific context |
| 8 | (EPSG codes, bounding boxes, CRS info, spatial file paths, model names). |
| 9 | |
| 10 | ## Step 1 -- Set the search path |
| 11 | |
| 12 | ```bash |
| 13 | ALL_PROJECTS="$HOME/.claude/projects/*/*.jsonl" |
| 14 | CURRENT_PROJECT="$HOME/.claude/projects/$(echo "$PWD" | sed 's|[/_]|-|g')/*.jsonl" |
| 15 | ``` |
| 16 | |
| 17 | Use `$CURRENT_PROJECT` if any argument is `--here`, otherwise use `$ALL_PROJECTS`. |
| 18 | Store the chosen glob in `SEARCH_PATH`. |
| 19 | |
| 20 | Check whether the `--geo` flag is present. |
| 21 | |
| 22 | ## Step 2 -- Query with Python |
| 23 | |
| 24 | Run the following Python script via `python3 -c "..."`, substituting |
| 25 | `<SEARCH_PATH>` and `<KEYWORD>` with the resolved values. Escape any |
| 26 | single quotes in `<KEYWORD>` before embedding it. |
| 27 | |
| 28 | ```bash |
| 29 | python3 -c " |
| 30 | import json, glob, os |
| 31 | |
| 32 | SEARCH_PATH = '<SEARCH_PATH>' |
| 33 | KEYWORD = '<KEYWORD>'.lower() |
| 34 | LIMIT = 40 |
| 35 | |
| 36 | files = sorted(glob.glob(os.path.expanduser(SEARCH_PATH))) |
| 37 | results = [] |
| 38 | |
| 39 | for fpath in files: |
| 40 | parts = fpath.split('/') |
| 41 | try: |
| 42 | proj_idx = parts.index('projects') + 1 |
| 43 | project = parts[proj_idx] if proj_idx < len(parts) else 'unknown' |
| 44 | except ValueError: |
| 45 | project = 'unknown' |
| 46 | |
| 47 | with open(fpath, 'r', errors='replace') as f: |
| 48 | for line in f: |
| 49 | try: |
| 50 | obj = json.loads(line) |
| 51 | except (json.JSONDecodeError, ValueError): |
| 52 | continue |
| 53 | |
| 54 | msg = obj.get('message') |
| 55 | if not isinstance(msg, dict): |
| 56 | continue |
| 57 | role = msg.get('role') |
| 58 | if role not in ('user', 'assistant'): |
| 59 | continue |
| 60 | |
| 61 | content = msg.get('content', '') |
| 62 | if isinstance(content, list): |
| 63 | text = ' '.join( |
| 64 | c.get('text', '') |
| 65 | for c in content |
| 66 | if isinstance(c, dict) and 'text' in c |
| 67 | ) |
| 68 | elif isinstance(content, str): |
| 69 | text = content |
| 70 | else: |
| 71 | continue |
| 72 | |
| 73 | if KEYWORD not in text.lower(): |
| 74 | continue |
| 75 | |
| 76 | ts = obj.get('timestamp', '') |
| 77 | snippet = text[:1500] |
| 78 | results.append({ |
| 79 | 'project': project, |
| 80 | 'ts': ts[:16].replace('T', ' ') if ts else '', |
| 81 | 'role': role, |
| 82 | 'content': snippet, |
| 83 | }) |
| 84 | |
| 85 | if len(results) >= LIMIT: |
| 86 | break |
| 87 | if len(results) >= LIMIT: |
| 88 | break |
| 89 | |
| 90 | print(f'Found {len(results)} results (limit {LIMIT})') |
| 91 | print('---') |
| 92 | for i, r in enumerate(results): |
| 93 | print(f'[{i+1}] project={r[\"project\"]} ts={r[\"ts\"]} role={r[\"role\"]}') |
| 94 | print(r['content'][:800]) |
| 95 | print('---') |
| 96 | " |
| 97 | ``` |
| 98 | |
| 99 | ## Step 3 -- Handle large result sets |
| 100 | |
| 101 | If Step 2 reports exactly 40 results (limit hit), the keyword is common. Run a |
| 102 | counting pass to understand the scope: |
| 103 | |
| 104 | ```bash |
| 105 | python3 -c " |
| 106 | import json, glob, os |
| 107 | |
| 108 | SEARCH_PATH = '<SEARCH_PATH>' |
| 109 | KEYWORD = '<KEYWORD>'.lower() |
| 110 | |
| 111 | files = sorted(glob.glob(os.path.expanduser(SEARCH_PATH))) |
| 112 | total = 0 |
| 113 | by_project = {} |
| 114 | |
| 115 | for fpath in files: |
| 116 | parts = fpath.split('/') |
| 117 | try: |
| 118 | proj_idx = parts.index('projects') + 1 |
| 119 | project = parts[proj_idx] if proj_idx < len(parts) else 'unknown' |
| 120 | except ValueError: |
| 121 | project = 'unknown' |
| 122 | |
| 123 | with open(fpath, 'r', errors='replace') as f: |
| 124 | for line in f: |
| 125 | try: |
| 126 | obj = json.loads(line) |
| 127 | except (json.JSONDecodeError, ValueError): |
| 128 | continue |
| 129 | msg = obj.get('message') |
| 130 | if not isinstance(msg, dict): |
| 131 | continue |
| 132 | role = msg.get('role') |
| 133 | if role not in ('user', 'assistant'): |
| 134 | continue |
| 135 | content = msg.get('content', '') |
| 136 | if isinstance(content, list): |
| 137 | text = ' '.join( |
| 138 | c.get('text', '') |
| 139 | for c in content |
| 140 | if isinstance(c, dict) and 'text' in c |
| 141 | ) |
| 142 | elif isinstance(content, str): |
| 143 | text = content |
| 144 | else: |
| 145 | continue |
| 146 | if KEYWORD in text.lower(): |
| 147 | total += 1 |
| 148 | by_project[project] = by_project.get(project, 0) + 1 |
| 149 | |
| 150 | print(f'Total matches: {total}') |
| 151 | for proj, cnt in sorted(by_project.items(), key=lambda x: -x[1]): |
| 152 | print(f' {proj}: {cnt}') |
| 153 | " |
| 154 | ``` |
| 155 | |
| 156 | Use this breakdown to decide whether to: |
| 157 | - |