linear-mcp-fast 0.1.0__py3-none-any.whl

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 (39) hide show
  1. ccl_chromium_reader/__init__.py +2 -0
  2. ccl_chromium_reader/ccl_chromium_cache.py +1335 -0
  3. ccl_chromium_reader/ccl_chromium_filesystem.py +302 -0
  4. ccl_chromium_reader/ccl_chromium_history.py +357 -0
  5. ccl_chromium_reader/ccl_chromium_indexeddb.py +1060 -0
  6. ccl_chromium_reader/ccl_chromium_localstorage.py +454 -0
  7. ccl_chromium_reader/ccl_chromium_notifications.py +268 -0
  8. ccl_chromium_reader/ccl_chromium_profile_folder.py +568 -0
  9. ccl_chromium_reader/ccl_chromium_sessionstorage.py +368 -0
  10. ccl_chromium_reader/ccl_chromium_snss2.py +332 -0
  11. ccl_chromium_reader/ccl_shared_proto_db_downloads.py +189 -0
  12. ccl_chromium_reader/common.py +19 -0
  13. ccl_chromium_reader/download_common.py +78 -0
  14. ccl_chromium_reader/profile_folder_protocols.py +276 -0
  15. ccl_chromium_reader/serialization_formats/__init__.py +0 -0
  16. ccl_chromium_reader/serialization_formats/ccl_blink_value_deserializer.py +401 -0
  17. ccl_chromium_reader/serialization_formats/ccl_easy_chromium_pickle.py +133 -0
  18. ccl_chromium_reader/serialization_formats/ccl_protobuff.py +276 -0
  19. ccl_chromium_reader/serialization_formats/ccl_v8_value_deserializer.py +627 -0
  20. ccl_chromium_reader/storage_formats/__init__.py +0 -0
  21. ccl_chromium_reader/storage_formats/ccl_leveldb.py +582 -0
  22. ccl_simplesnappy/__init__.py +1 -0
  23. ccl_simplesnappy/ccl_simplesnappy.py +306 -0
  24. linear_mcp_fast/__init__.py +8 -0
  25. linear_mcp_fast/__main__.py +6 -0
  26. linear_mcp_fast/reader.py +433 -0
  27. linear_mcp_fast/server.py +367 -0
  28. linear_mcp_fast/store_detector.py +117 -0
  29. linear_mcp_fast-0.1.0.dist-info/METADATA +160 -0
  30. linear_mcp_fast-0.1.0.dist-info/RECORD +39 -0
  31. linear_mcp_fast-0.1.0.dist-info/WHEEL +5 -0
  32. linear_mcp_fast-0.1.0.dist-info/entry_points.txt +2 -0
  33. linear_mcp_fast-0.1.0.dist-info/top_level.txt +4 -0
  34. tools_and_utilities/Chromium_dump_local_storage.py +111 -0
  35. tools_and_utilities/Chromium_dump_session_storage.py +92 -0
  36. tools_and_utilities/benchmark.py +35 -0
  37. tools_and_utilities/ccl_chrome_audit.py +651 -0
  38. tools_and_utilities/dump_indexeddb_details.py +59 -0
  39. tools_and_utilities/dump_leveldb.py +53 -0
@@ -0,0 +1,367 @@
1
+ """
2
+ MCP Server for Linear Local Cache.
3
+
4
+ Provides fast, read-only access to Linear data from local cache.
5
+ For write operations, use the official Linear MCP server.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ from mcp.server.fastmcp import FastMCP
11
+
12
+ from .reader import LinearLocalReader
13
+
14
+ mcp = FastMCP(
15
+ "Linear Local Cache",
16
+ instructions=(
17
+ "Fast, read-only access to Linear data from the local Linear.app cache on macOS. "
18
+ "Data freshness depends on Linear.app's last sync. "
19
+ "For write operations (comments, updates), use the official Linear MCP server."
20
+ ),
21
+ )
22
+
23
+ _reader: LinearLocalReader | None = None
24
+
25
+
26
+ def get_reader() -> LinearLocalReader:
27
+ """Get or create the LinearLocalReader instance."""
28
+ global _reader
29
+ if _reader is None:
30
+ _reader = LinearLocalReader()
31
+ return _reader
32
+
33
+
34
+ def _parse_datetime(dt_value: Any) -> float | None:
35
+ """Parse a datetime value to Unix timestamp."""
36
+ if dt_value is None:
37
+ return None
38
+ if isinstance(dt_value, (int, float)):
39
+ if dt_value > 1e12:
40
+ return dt_value / 1000
41
+ return dt_value
42
+ if isinstance(dt_value, str):
43
+ from datetime import datetime
44
+ try:
45
+ dt_str = dt_value.replace("Z", "+00:00")
46
+ dt = datetime.fromisoformat(dt_str)
47
+ return dt.timestamp()
48
+ except ValueError:
49
+ return None
50
+ return None
51
+
52
+
53
+ @mcp.tool()
54
+ def list_issues(
55
+ assignee: str | None = None,
56
+ team: str | None = None,
57
+ state_type: str | None = None,
58
+ priority: int | None = None,
59
+ limit: int = 50,
60
+ ) -> dict[str, Any]:
61
+ """
62
+ List issues from local cache with optional filters.
63
+
64
+ Args:
65
+ assignee: Filter by assignee name (partial match)
66
+ team: Filter by team key (e.g., 'UK')
67
+ state_type: Filter by state type (started, unstarted, completed, canceled, backlog)
68
+ priority: Filter by priority (1=Urgent, 2=High, 3=Medium, 4=Low)
69
+ limit: Maximum number of issues (default 50, max 100)
70
+
71
+ Returns:
72
+ Dictionary with issues array and totalCount
73
+ """
74
+ reader = get_reader()
75
+ limit = min(limit, 100)
76
+
77
+ assignee_id = None
78
+ if assignee:
79
+ user = reader.find_user(assignee)
80
+ if user:
81
+ assignee_id = user["id"]
82
+ else:
83
+ return {"issues": [], "totalCount": 0}
84
+
85
+ team_id = None
86
+ if team:
87
+ team_obj = reader.find_team(team)
88
+ if team_obj:
89
+ team_id = team_obj["id"]
90
+ else:
91
+ return {"issues": [], "totalCount": 0}
92
+
93
+ all_issues = sorted(
94
+ reader.issues.values(),
95
+ key=lambda x: (x.get("priority") or 4, x.get("updatedAt") or ""),
96
+ )
97
+
98
+ filtered = []
99
+ for issue in all_issues:
100
+ if assignee_id and issue.get("assigneeId") != assignee_id:
101
+ continue
102
+ if team_id and issue.get("teamId") != team_id:
103
+ continue
104
+ if state_type and reader.get_state_type(issue.get("stateId", "")) != state_type:
105
+ continue
106
+ if priority is not None and issue.get("priority") != priority:
107
+ continue
108
+ filtered.append(issue)
109
+
110
+ total_count = len(filtered)
111
+ page = filtered[:limit]
112
+
113
+ results = []
114
+ for issue in page:
115
+ results.append({
116
+ "identifier": issue.get("identifier"),
117
+ "title": issue.get("title"),
118
+ "priority": issue.get("priority"),
119
+ "state": reader.get_state_name(issue.get("stateId", "")),
120
+ "stateType": reader.get_state_type(issue.get("stateId", "")),
121
+ "assignee": reader.get_user_name(issue.get("assigneeId")),
122
+ "dueDate": issue.get("dueDate"),
123
+ })
124
+
125
+ return {"issues": results, "totalCount": total_count}
126
+
127
+
128
+ @mcp.tool()
129
+ def get_issue(identifier: str) -> dict[str, Any] | None:
130
+ """
131
+ Get issue details by identifier (e.g., 'UK-55').
132
+
133
+ Args:
134
+ identifier: Issue identifier like 'UK-55'
135
+
136
+ Returns:
137
+ Issue details with comments, or None if not found
138
+ """
139
+ reader = get_reader()
140
+ issue = reader.get_issue_by_identifier(identifier)
141
+
142
+ if not issue:
143
+ return None
144
+
145
+ comments = reader.get_comments_for_issue(issue["id"])
146
+ enriched_comments = []
147
+ for comment in comments:
148
+ user = reader.users.get(comment.get("userId", ""), {})
149
+ enriched_comments.append({
150
+ "author": user.get("name", "Unknown"),
151
+ "body": comment.get("body", ""),
152
+ "createdAt": comment.get("createdAt"),
153
+ })
154
+
155
+ return {
156
+ "identifier": issue.get("identifier"),
157
+ "title": issue.get("title"),
158
+ "description": issue.get("description"),
159
+ "priority": issue.get("priority"),
160
+ "estimate": issue.get("estimate"),
161
+ "state": reader.get_state_name(issue.get("stateId", "")),
162
+ "stateType": reader.get_state_type(issue.get("stateId", "")),
163
+ "assignee": reader.get_user_name(issue.get("assigneeId")),
164
+ "project": reader.get_project_name(issue.get("projectId")),
165
+ "dueDate": issue.get("dueDate"),
166
+ "createdAt": issue.get("createdAt"),
167
+ "updatedAt": issue.get("updatedAt"),
168
+ "comments": enriched_comments,
169
+ "url": f"https://linear.app/issue/{issue.get('identifier')}",
170
+ }
171
+
172
+
173
+ @mcp.tool()
174
+ def search_issues(query: str, limit: int = 20) -> dict[str, Any]:
175
+ """
176
+ Search issues by title.
177
+
178
+ Args:
179
+ query: Search query (case-insensitive)
180
+ limit: Maximum results (default 20, max 100)
181
+
182
+ Returns:
183
+ Dictionary with matching issues
184
+ """
185
+ reader = get_reader()
186
+ limit = min(limit, 100)
187
+ query_lower = query.lower()
188
+
189
+ all_issues = sorted(
190
+ reader.issues.values(),
191
+ key=lambda x: (x.get("priority") or 4, x.get("id", "")),
192
+ )
193
+
194
+ filtered = []
195
+ for issue in all_issues:
196
+ title = issue.get("title", "") or ""
197
+ identifier = issue.get("identifier", "") or ""
198
+ if query_lower in title.lower() or query_lower in identifier.lower():
199
+ filtered.append(issue)
200
+
201
+ match_count = len(filtered)
202
+ page = filtered[:limit]
203
+
204
+ results = []
205
+ for issue in page:
206
+ results.append({
207
+ "identifier": issue.get("identifier"),
208
+ "title": issue.get("title"),
209
+ "state": reader.get_state_name(issue.get("stateId", "")),
210
+ "stateType": reader.get_state_type(issue.get("stateId", "")),
211
+ })
212
+
213
+ return {"issues": results, "matchCount": match_count}
214
+
215
+
216
+ @mcp.tool()
217
+ def get_my_issues(
218
+ name: str,
219
+ state_type: str | None = None,
220
+ limit: int = 30,
221
+ ) -> dict[str, Any]:
222
+ """
223
+ Get issues assigned to a user.
224
+
225
+ Args:
226
+ name: User name to search for
227
+ state_type: Optional filter (started, unstarted, completed, canceled, backlog)
228
+ limit: Maximum issues (default 30, max 100)
229
+
230
+ Returns:
231
+ User info with their issues
232
+ """
233
+ reader = get_reader()
234
+ limit = min(limit, 100)
235
+
236
+ user = reader.find_user(name)
237
+ if not user:
238
+ return {"error": f"User '{name}' not found"}
239
+
240
+ all_issues = sorted(
241
+ reader.get_issues_for_user(user["id"]),
242
+ key=lambda x: (x.get("priority") or 4, x.get("id", "")),
243
+ )
244
+
245
+ counts_by_state: dict[str, int] = {}
246
+ for issue in all_issues:
247
+ issue_state_type = reader.get_state_type(issue.get("stateId", ""))
248
+ counts_by_state[issue_state_type] = counts_by_state.get(issue_state_type, 0) + 1
249
+
250
+ if state_type:
251
+ all_issues = [
252
+ i for i in all_issues
253
+ if reader.get_state_type(i.get("stateId", "")) == state_type
254
+ ]
255
+
256
+ page = all_issues[:limit]
257
+
258
+ results = []
259
+ for issue in page:
260
+ results.append({
261
+ "identifier": issue.get("identifier"),
262
+ "title": issue.get("title"),
263
+ "priority": issue.get("priority"),
264
+ "state": reader.get_state_name(issue.get("stateId", "")),
265
+ "stateType": reader.get_state_type(issue.get("stateId", "")),
266
+ "dueDate": issue.get("dueDate"),
267
+ })
268
+
269
+ return {
270
+ "user": {"name": user.get("name"), "email": user.get("email")},
271
+ "totalIssues": sum(counts_by_state.values()),
272
+ "countsByState": counts_by_state,
273
+ "issues": results,
274
+ }
275
+
276
+
277
+ @mcp.tool()
278
+ def list_teams() -> list[dict[str, Any]]:
279
+ """
280
+ List all teams with issue counts.
281
+
282
+ Returns:
283
+ List of teams
284
+ """
285
+ reader = get_reader()
286
+ results = []
287
+
288
+ for team in reader.teams.values():
289
+ issue_count = sum(
290
+ 1 for i in reader.issues.values() if i.get("teamId") == team["id"]
291
+ )
292
+ results.append({
293
+ "key": team.get("key"),
294
+ "name": team.get("name"),
295
+ "issueCount": issue_count,
296
+ })
297
+
298
+ results.sort(key=lambda x: x.get("key", ""))
299
+ return results
300
+
301
+
302
+ @mcp.tool()
303
+ def list_projects(team: str | None = None) -> list[dict[str, Any]]:
304
+ """
305
+ List all projects with issue counts.
306
+
307
+ Args:
308
+ team: Optional team key to filter
309
+
310
+ Returns:
311
+ List of projects
312
+ """
313
+ reader = get_reader()
314
+
315
+ team_id = None
316
+ if team:
317
+ team_obj = reader.find_team(team)
318
+ if team_obj:
319
+ team_id = team_obj["id"]
320
+ else:
321
+ return []
322
+
323
+ results = []
324
+ for project in reader.projects.values():
325
+ if team_id and team_id not in project.get("teamIds", []):
326
+ continue
327
+
328
+ issue_count = sum(
329
+ 1 for i in reader.issues.values() if i.get("projectId") == project["id"]
330
+ )
331
+
332
+ results.append({
333
+ "name": project.get("name"),
334
+ "state": project.get("state"),
335
+ "issueCount": issue_count,
336
+ "startDate": project.get("startDate"),
337
+ "targetDate": project.get("targetDate"),
338
+ })
339
+
340
+ results.sort(key=lambda x: x.get("name", "") or "")
341
+ return results
342
+
343
+
344
+ @mcp.tool()
345
+ def get_summary() -> dict[str, Any]:
346
+ """
347
+ Get a summary of local cache data.
348
+
349
+ Returns:
350
+ Counts of teams, users, issues, projects, comments
351
+ """
352
+ reader = get_reader()
353
+ summary = reader.get_summary()
354
+
355
+ # Add state breakdown
356
+ state_counts: dict[str, int] = {}
357
+ for issue in reader.issues.values():
358
+ state_type = reader.get_state_type(issue.get("stateId", ""))
359
+ state_counts[state_type] = state_counts.get(state_type, 0) + 1
360
+
361
+ summary["issuesByState"] = state_counts
362
+ return summary
363
+
364
+
365
+ def main():
366
+ """Run the MCP server."""
367
+ mcp.run()
@@ -0,0 +1,117 @@
1
+ """
2
+ Auto-detect Linear IndexedDB object store hashes by sampling records.
3
+
4
+ Linear uses hash-based object store names that may change between versions.
5
+ This module detects stores by examining the structure of their records.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from typing import Any
10
+
11
+ from ccl_chromium_reader import ccl_chromium_indexeddb # type: ignore
12
+
13
+
14
+ @dataclass
15
+ class DetectedStores:
16
+ """Container for detected object store names."""
17
+
18
+ issues: str | None = None
19
+ teams: str | None = None
20
+ users: list[str] | None = None
21
+ workflow_states: list[str] | None = None
22
+ comments: str | None = None
23
+ projects: str | None = None
24
+
25
+
26
+ def _is_issue_record(record: dict[str, Any]) -> bool:
27
+ """Check if a record looks like an issue."""
28
+ required = {"number", "teamId", "stateId", "title"}
29
+ return required.issubset(record.keys())
30
+
31
+
32
+ def _is_user_record(record: dict[str, Any]) -> bool:
33
+ """Check if a record looks like a user."""
34
+ required = {"name", "displayName", "email"}
35
+ has_required = required.issubset(record.keys())
36
+ has_avatar = "avatarUrl" in record or "avatar" in record
37
+ return has_required and has_avatar
38
+
39
+
40
+ def _is_team_record(record: dict[str, Any]) -> bool:
41
+ """Check if a record looks like a team."""
42
+ if not {"key", "name"}.issubset(record.keys()):
43
+ return False
44
+ key = record.get("key")
45
+ if not isinstance(key, str):
46
+ return False
47
+ return key.isupper() and key.isalpha() and len(key) <= 10
48
+
49
+
50
+ def _is_workflow_state_record(record: dict[str, Any]) -> bool:
51
+ """Check if a record looks like a workflow state."""
52
+ if not {"name", "type", "color"}.issubset(record.keys()):
53
+ return False
54
+ state_type = record.get("type")
55
+ valid_types = {"started", "unstarted", "completed", "canceled", "backlog"}
56
+ return state_type in valid_types
57
+
58
+
59
+ def _is_comment_record(record: dict[str, Any]) -> bool:
60
+ """Check if a record looks like a comment."""
61
+ required = {"issueId", "userId", "bodyData", "createdAt"}
62
+ return required.issubset(record.keys())
63
+
64
+
65
+ def _is_project_record(record: dict[str, Any]) -> bool:
66
+ """Check if a record looks like a project."""
67
+ required = {"name", "description", "teamIds", "startDate", "targetDate", "statusId"}
68
+ return required.issubset(record.keys())
69
+
70
+
71
+ def detect_stores(db: ccl_chromium_indexeddb.WrappedDatabase) -> DetectedStores:
72
+ """
73
+ Detect object stores by sampling their first record.
74
+
75
+ Args:
76
+ db: The wrapped IndexedDB database to scan.
77
+
78
+ Returns:
79
+ DetectedStores with detected store names for each entity type.
80
+ """
81
+ result = DetectedStores(users=[], workflow_states=[])
82
+
83
+ for store_name in db.object_store_names:
84
+ if store_name is None or store_name.startswith("_") or "_partial" in store_name:
85
+ continue
86
+
87
+ try:
88
+ store = db[store_name]
89
+ for record in store.iterate_records():
90
+ val = record.value
91
+ if not isinstance(val, dict):
92
+ break
93
+
94
+ if _is_issue_record(val) and result.issues is None:
95
+ result.issues = store_name
96
+ elif _is_team_record(val) and result.teams is None:
97
+ result.teams = store_name
98
+ elif _is_user_record(val) and store_name not in (result.users or []):
99
+ if result.users is None:
100
+ result.users = []
101
+ result.users.append(store_name)
102
+ elif _is_workflow_state_record(val) and store_name not in (
103
+ result.workflow_states or []
104
+ ):
105
+ if result.workflow_states is None:
106
+ result.workflow_states = []
107
+ result.workflow_states.append(store_name)
108
+ elif _is_comment_record(val) and result.comments is None:
109
+ result.comments = store_name
110
+ elif _is_project_record(val) and result.projects is None:
111
+ result.projects = store_name
112
+
113
+ break # Only check first record
114
+ except Exception:
115
+ continue
116
+
117
+ return result
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.4
2
+ Name: linear-mcp-fast
3
+ Version: 0.1.0
4
+ Summary: Fast MCP server for Linear - reads from Linear.app's local cache on macOS
5
+ Author: everything-chalna
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/everything-chalna/linear-mcp-fast
8
+ Project-URL: Repository, https://github.com/everything-chalna/linear-mcp-fast
9
+ Project-URL: Issues, https://github.com/everything-chalna/linear-mcp-fast/issues
10
+ Keywords: linear,mcp,cache,indexeddb,macos
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: mcp>=1.0.0
23
+ Requires-Dist: brotli>=1.0.0
24
+
25
+ # linear-mcp-fast
26
+
27
+ Fast, read-only MCP server for Linear that reads from Linear.app's local cache on macOS.
28
+
29
+ **Why?**
30
+ - **Instant**: No API calls, reads directly from local IndexedDB cache
31
+ - **Offline**: Works without internet
32
+ - **Lower context**: Smaller responses for AI assistants
33
+
34
+ **Use with**: Official Linear MCP for write operations (comments, updates).
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ pip install linear-mcp-fast
40
+ # or
41
+ uv pip install linear-mcp-fast
42
+ ```
43
+
44
+ ## Setup
45
+
46
+ ### Claude Code
47
+
48
+ ```bash
49
+ # 1. Add linear-mcp-fast (reads from local cache)
50
+ claude mcp add linear-fast -- uvx linear-mcp-fast
51
+
52
+ # 2. Add official Linear MCP (for writes)
53
+ claude mcp add --transport http linear https://mcp.linear.app/mcp
54
+ ```
55
+
56
+ Now you have:
57
+ - `linear-fast` → Fast reads from local cache
58
+ - `linear` → Writes (comments, updates)
59
+
60
+ ### Claude Desktop
61
+
62
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
63
+
64
+ ```json
65
+ {
66
+ "mcpServers": {
67
+ "linear-fast": {
68
+ "command": "uvx",
69
+ "args": ["linear-mcp-fast"]
70
+ },
71
+ "linear": {
72
+ "command": "npx",
73
+ "args": ["-y", "mcp-remote", "https://mcp.linear.app/mcp"]
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ ### Cursor / VS Code
80
+
81
+ ```json
82
+ {
83
+ "mcpServers": {
84
+ "linear-fast": {
85
+ "command": "uvx",
86
+ "args": ["linear-mcp-fast"]
87
+ },
88
+ "linear": {
89
+ "command": "npx",
90
+ "args": ["-y", "mcp-remote", "https://mcp.linear.app/mcp"]
91
+ }
92
+ }
93
+ }
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Prerequisites
99
+
100
+ - **macOS** (Linear.app stores cache in `~/Library/Application Support/Linear/`)
101
+ - **Linear.app** installed and opened at least once
102
+
103
+ ---
104
+
105
+ ## Available Tools
106
+
107
+ ### Reading (linear-fast)
108
+
109
+ | Tool | Description |
110
+ |------|-------------|
111
+ | `list_issues` | List issues with filters (assignee, team, state, priority) |
112
+ | `get_issue` | Get issue details with comments |
113
+ | `search_issues` | Search issues by title |
114
+ | `get_my_issues` | Get issues assigned to a user |
115
+ | `list_teams` | List all teams |
116
+ | `list_projects` | List all projects |
117
+ | `get_summary` | Get cache summary |
118
+
119
+ ### Writing (official Linear MCP)
120
+
121
+ Use the official Linear MCP (`linear`) for:
122
+ - Creating/updating issues
123
+ - Adding comments
124
+ - Changing status
125
+
126
+ ---
127
+
128
+ ## Troubleshooting
129
+
130
+ ### "Linear database not found"
131
+
132
+ Linear.app needs to be installed and opened at least once:
133
+ ```bash
134
+ ls ~/Library/Application\ Support/Linear/IndexedDB/
135
+ ```
136
+
137
+ ### Data seems stale
138
+
139
+ Local cache is updated when Linear.app syncs. Open Linear.app to refresh.
140
+
141
+ ---
142
+
143
+ ## How It Works
144
+
145
+ ```
146
+ Linear.app (Electron)
147
+ ↓ syncs to
148
+ IndexedDB (LevelDB format)
149
+ ~/Library/Application Support/Linear/IndexedDB/...
150
+ ↓ read by
151
+ linear-mcp-fast
152
+ ↓ provides
153
+ Fast read-only access to issues, users, teams, comments
154
+ ```
155
+
156
+ ---
157
+
158
+ ## License
159
+
160
+ MIT
@@ -0,0 +1,39 @@
1
+ ccl_chromium_reader/__init__.py,sha256=ktLJfrcRmDRw8kc81l71lHYNjGuqXCkzBMYEvRhQLP4,93
2
+ ccl_chromium_reader/ccl_chromium_cache.py,sha256=JE5DnhZTJ8QPuaTSJykzNtm7SrKV92d-hJR3ZEkaYQ0,49588
3
+ ccl_chromium_reader/ccl_chromium_filesystem.py,sha256=R4bBkDvtbw0Y6lZjg98bVfJaRpvD9Ckvl6gThVBViT8,12949
4
+ ccl_chromium_reader/ccl_chromium_history.py,sha256=ULexw7SV8vR0Mwx-ZMcstB-eSDLcrUSUkqji1tMS2CQ,11910
5
+ ccl_chromium_reader/ccl_chromium_indexeddb.py,sha256=arHjNf6fWgYwmK7IKOY59Dm3Z9T-X2t1TiIY3bE-Xyk,44498
6
+ ccl_chromium_reader/ccl_chromium_localstorage.py,sha256=DHr-KSxDJa49l4DjraoFSGDUsFVK1wpp2hB6EShx4ug,20269
7
+ ccl_chromium_reader/ccl_chromium_notifications.py,sha256=9pDKdMZFNTXfSN0lhwcPUd3XTS9ulwAjqWcujjJZiIU,10876
8
+ ccl_chromium_reader/ccl_chromium_profile_folder.py,sha256=psN3m40iha3W8Fn-Sb8V74HE4_hrcGKNyKJmZdtCV58,29076
9
+ ccl_chromium_reader/ccl_chromium_sessionstorage.py,sha256=4hHogY7pAMnzbTZ3Ikv42jd42lBDyjWp2UjTSp2nbCo,15848
10
+ ccl_chromium_reader/ccl_chromium_snss2.py,sha256=8rWyncs4l_l3dB-TaNPPIWjIxc3yWvBHwgkV0sm758s,12336
11
+ ccl_chromium_reader/ccl_shared_proto_db_downloads.py,sha256=9cEGFhlwXwHhtgXUt3tJ2IxNPgDPBwGRgdpInuyTfNI,8413
12
+ ccl_chromium_reader/common.py,sha256=ciCeGUUyXzz0IQ0nPFirEHVJB9uat4nnGp123Mdq9eE,618
13
+ ccl_chromium_reader/download_common.py,sha256=u4jFOdDqfBqh9t8TV5iiGoy3P2lJRhoUx60V5eLSC40,2977
14
+ ccl_chromium_reader/profile_folder_protocols.py,sha256=1oBpowQraWJ_ckTX4xrjRkNPmZEX8bhad6Yrxg8kqP8,12306
15
+ ccl_chromium_reader/serialization_formats/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ ccl_chromium_reader/serialization_formats/ccl_blink_value_deserializer.py,sha256=jqHf9I4fwXam1DMS5iVcbX_G-m-TQcaC_CwhyNylQJM,18647
17
+ ccl_chromium_reader/serialization_formats/ccl_easy_chromium_pickle.py,sha256=YQ5Dofla2OSR-3p32FO1rGTXn8qD7942lxU6VE_-PRE,4388
18
+ ccl_chromium_reader/serialization_formats/ccl_protobuff.py,sha256=3OJUcy2ZwR1qU8NrjEyDItt5uqMpo0TM0ixWqFNbb2s,8990
19
+ ccl_chromium_reader/serialization_formats/ccl_v8_value_deserializer.py,sha256=c_bJHGVzqKs1X0dw0EwojNmoh5d1THI1OiKzgu-wLWY,22979
20
+ ccl_chromium_reader/storage_formats/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ ccl_chromium_reader/storage_formats/ccl_leveldb.py,sha256=KEM6hQryZZrWZ6KafaHYX69QAMT07BuCvfBIpkAeFsA,22989
22
+ ccl_simplesnappy/__init__.py,sha256=OqArK0MfdVl2oMw1MwpWSYWWVLFT-VLIWnEXXCsacJo,59
23
+ ccl_simplesnappy/ccl_simplesnappy.py,sha256=dLv1wejr2vCa2b_ZinozXVfcSsZIzJrt5ZkyxA3cQXA,10461
24
+ linear_mcp_fast/__init__.py,sha256=T-ioPzoZXC3a_zLilZuDBCXUn8nDjvITS7svk7BwWmY,138
25
+ linear_mcp_fast/__main__.py,sha256=2wkhXADcE2oGdtEpGrIvvEe9YGKjpwnJ3DBWghkVQKk,124
26
+ linear_mcp_fast/reader.py,sha256=lf-bHccLSzmP0Ez4UeRLLYoYvVwlZOByWnfcUTtuuV0,16249
27
+ linear_mcp_fast/server.py,sha256=cz-kSNy4y4xSRJRx_x9IyNl7TlwU9U3rA0pH1ro3QWs,10389
28
+ linear_mcp_fast/store_detector.py,sha256=5cnTwS-LPsgS9NivNxKa3bPv4mwAnZsi40-kXszejMI,4142
29
+ tools_and_utilities/Chromium_dump_local_storage.py,sha256=gG-pKFFk6lo332LQy2JvInlQh9Zldm5zAsuibb-dBkQ,4337
30
+ tools_and_utilities/Chromium_dump_session_storage.py,sha256=17BKFWioo6fPwYkH58QycJCA-z85RtWMBXsJ_29hHQs,3484
31
+ tools_and_utilities/benchmark.py,sha256=fyD5U6yI7Y0TkyhYtvvaHyk9Y2jJe2yxYWoFPQWypzA,1089
32
+ tools_and_utilities/ccl_chrome_audit.py,sha256=irGyYJae0apZDZCn23jMKmY3tYQgWyZEL8vdUBcHLZk,24695
33
+ tools_and_utilities/dump_indexeddb_details.py,sha256=ipNWLKPQoSNhCtPHKWvMWpKu8FhCnvc4Rciyx-90boI,2298
34
+ tools_and_utilities/dump_leveldb.py,sha256=hj7QnOHG64KK2fKsZ9qQOVqUUmHUtxUZqPYl4EZJO9U,1882
35
+ linear_mcp_fast-0.1.0.dist-info/METADATA,sha256=0aRAuDTEJ6TQDu1tEkJQAtCkXbEftJ1CLCukOGdg8p8,3691
36
+ linear_mcp_fast-0.1.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
37
+ linear_mcp_fast-0.1.0.dist-info/entry_points.txt,sha256=Aa98tAkWz_08mS_SRyfyx0k3PuMBQoMygT88HCKMyWk,57
38
+ linear_mcp_fast-0.1.0.dist-info/top_level.txt,sha256=j-O2BoBpFBpGyTl2V1cp0ZjxZAQwpkweeNxG4BcQ7io,73
39
+ linear_mcp_fast-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ linear-mcp-fast = linear_mcp_fast:main
@@ -0,0 +1,4 @@
1
+ ccl_chromium_reader
2
+ ccl_simplesnappy
3
+ linear_mcp_fast
4
+ tools_and_utilities