loreconvo 0.3.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.
@@ -0,0 +1,307 @@
1
+ """LoreConvo SessionStart auto-load hook.
2
+
3
+ Receives session metadata via stdin JSON from Claude Code's SessionStart hook.
4
+ Queries the LoreConvo SQLite database for recent sessions matching the current
5
+ working directory (project), then outputs a context summary to stdout.
6
+
7
+ Claude Code injects stdout content into the session as system context.
8
+ Designed to run within the hook timeout window.
9
+
10
+ Scoring logic (higher = shown first):
11
+ +3 session has open questions (most actionable context)
12
+ +2 session has >= 2 decisions
13
+ +1 session has artifacts
14
+ +2 started within last 24 hours
15
+ +1 started within last 3 days
16
+ -2 no summary, no decisions, no open questions, no artifacts (noise)
17
+
18
+ Sessions that score <= 0 and are not the only results are filtered out.
19
+ Total formatted context is capped at MAX_CONTEXT_CHARS to avoid bloat.
20
+ """
21
+
22
+ import json
23
+ import os
24
+ import sys
25
+ import sqlite3
26
+ from datetime import datetime, timedelta
27
+ from pathlib import Path
28
+
29
+
30
+ MAX_CONTEXT_CHARS = 4000 # Soft cap on total output length
31
+
32
+
33
+ def get_db_path():
34
+ """Get database path, matching core/config.py logic."""
35
+ return os.environ.get("LORECONVO_DB", os.path.expanduser("~/.loreconvo/sessions.db"))
36
+
37
+
38
+ def score_session(session, now):
39
+ """Compute a signal quality score for a session dict.
40
+
41
+ Higher scores = more actionable context for the incoming session.
42
+ """
43
+ score = 0
44
+
45
+ # Open questions are highest signal -- unanswered items the user may continue
46
+ open_q_raw = session.get("open_questions", "[]")
47
+ try:
48
+ open_questions = json.loads(open_q_raw) if isinstance(open_q_raw, str) else (open_q_raw or [])
49
+ except (json.JSONDecodeError, TypeError):
50
+ open_questions = []
51
+ if open_questions:
52
+ score += 3
53
+
54
+ # Decisions (>= 2 means a substantive work session)
55
+ decisions_raw = session.get("decisions", "[]")
56
+ try:
57
+ decisions = json.loads(decisions_raw) if isinstance(decisions_raw, str) else (decisions_raw or [])
58
+ except (json.JSONDecodeError, TypeError):
59
+ decisions = []
60
+ if len(decisions) >= 2:
61
+ score += 2
62
+ elif len(decisions) == 1:
63
+ score += 1
64
+
65
+ # Artifacts indicate something was produced
66
+ artifacts_raw = session.get("artifacts", "[]")
67
+ try:
68
+ artifacts = json.loads(artifacts_raw) if isinstance(artifacts_raw, str) else (artifacts_raw or [])
69
+ except (json.JSONDecodeError, TypeError):
70
+ artifacts = []
71
+ if artifacts:
72
+ score += 1
73
+
74
+ # Recency bonus
75
+ start_raw = session.get("start_date", "")
76
+ if start_raw:
77
+ try:
78
+ start_dt = datetime.fromisoformat(start_raw)
79
+ age = now - start_dt
80
+ if age <= timedelta(hours=24):
81
+ score += 2
82
+ elif age <= timedelta(days=3):
83
+ score += 1
84
+ except (ValueError, TypeError):
85
+ pass
86
+
87
+ # Noise penalty: short/empty sessions add clutter
88
+ summary = session.get("summary", "") or ""
89
+ if not summary and not decisions and not open_questions and not artifacts:
90
+ score -= 2
91
+
92
+ return score
93
+
94
+
95
+ def query_recent_sessions(db_path, cwd, days_back=14, limit=10):
96
+ """Query LoreConvo for recent sessions, optionally filtered by project/cwd.
97
+
98
+ Fetches a wider window than the old hook (days_back=14, limit=10) so the
99
+ scoring pass has enough candidates to work with. The caller then scores,
100
+ filters, and caps before formatting.
101
+
102
+ Returns a list of session dicts.
103
+ """
104
+ if not os.path.exists(db_path):
105
+ return []
106
+
107
+ conn = sqlite3.connect(db_path)
108
+ conn.row_factory = sqlite3.Row
109
+ try:
110
+ cutoff = (datetime.now() - timedelta(days=days_back)).isoformat()
111
+
112
+ sessions = []
113
+
114
+ if cwd:
115
+ cursor = conn.execute(
116
+ """SELECT id, title, summary, decisions, artifacts,
117
+ open_questions, tags, start_date, end_date
118
+ FROM sessions
119
+ WHERE project LIKE ?
120
+ AND start_date >= ?
121
+ ORDER BY start_date DESC
122
+ LIMIT ?""",
123
+ (f"%{cwd}%", cutoff, limit),
124
+ )
125
+ sessions = [dict(row) for row in cursor.fetchall()]
126
+
127
+ # Fall back to most recent across all projects if no project matches
128
+ if not sessions:
129
+ cursor = conn.execute(
130
+ """SELECT id, title, summary, decisions, artifacts,
131
+ open_questions, tags, start_date, end_date
132
+ FROM sessions
133
+ ORDER BY start_date DESC
134
+ LIMIT ?""",
135
+ (limit,),
136
+ )
137
+ sessions = [dict(row) for row in cursor.fetchall()]
138
+
139
+ return sessions
140
+
141
+ except Exception as e:
142
+ sys.stderr.write(f"LoreConvo auto-load query error: {e}\n")
143
+ return []
144
+ finally:
145
+ conn.close()
146
+
147
+
148
+ def select_sessions(sessions, max_count=5):
149
+ """Score, filter, and rank sessions. Returns up to max_count best ones."""
150
+ if not sessions:
151
+ return []
152
+
153
+ now = datetime.now()
154
+ scored = [(score_session(s, now), s) for s in sessions]
155
+
156
+ # Sort by score desc, then recency desc for ties
157
+ scored.sort(key=lambda x: (x[0], x[1].get("start_date", "")), reverse=True)
158
+
159
+ # Filter out noise sessions as long as we still have enough good ones
160
+ good = [(sc, s) for sc, s in scored if sc > 0]
161
+ if not good:
162
+ # All sessions scored <= 0 -- keep top few rather than returning nothing
163
+ good = scored[:max_count]
164
+
165
+ return [s for _, s in good[:max_count]]
166
+
167
+
168
+ def format_context(sessions, cwd):
169
+ """Format session data into a concise context block for Claude.
170
+
171
+ Output is plain text that Claude Code injects into the session.
172
+ Includes open_questions (missing from old version) as they are highest-signal.
173
+ Enforces MAX_CONTEXT_CHARS soft cap to prevent system prompt bloat.
174
+ """
175
+ if not sessions:
176
+ return ""
177
+
178
+ lines = []
179
+ lines.append("# LoreConvo: Recent Session Context")
180
+ lines.append("")
181
+
182
+ if cwd:
183
+ project_name = os.path.basename(cwd) if cwd else "unknown"
184
+ lines.append(f"Recent sessions for project: {project_name}")
185
+ else:
186
+ lines.append("Recent sessions (no project filter):")
187
+ lines.append("")
188
+
189
+ total_chars = sum(len(l) for l in lines)
190
+
191
+ for i, session in enumerate(sessions, 1):
192
+ block = []
193
+ title = session.get("title") or "Untitled"
194
+ start = session.get("start_date", "")
195
+
196
+ date_str = ""
197
+ if start:
198
+ try:
199
+ dt = datetime.fromisoformat(start)
200
+ date_str = dt.strftime("%b %d, %Y %I:%M %p")
201
+ except (ValueError, TypeError):
202
+ date_str = start[:19] if start else ""
203
+
204
+ block.append(f"## Session {i}: {title}")
205
+ if date_str:
206
+ block.append(f"Date: {date_str}")
207
+
208
+ # Summary (truncated)
209
+ summary = session.get("summary") or ""
210
+ if summary:
211
+ truncated = summary[:400]
212
+ if len(summary) > 400:
213
+ truncated += "..."
214
+ block.append(f"Summary: {truncated}")
215
+
216
+ # Open questions -- highest signal, always include when present
217
+ open_q_raw = session.get("open_questions", "[]")
218
+ try:
219
+ open_questions = json.loads(open_q_raw) if isinstance(open_q_raw, str) else (open_q_raw or [])
220
+ except (json.JSONDecodeError, TypeError):
221
+ open_questions = []
222
+ if open_questions:
223
+ block.append("Open questions:")
224
+ for q in open_questions[:4]:
225
+ block.append(f" ? {q}")
226
+
227
+ # Decisions
228
+ decisions_raw = session.get("decisions", "[]")
229
+ try:
230
+ decisions = json.loads(decisions_raw) if isinstance(decisions_raw, str) else (decisions_raw or [])
231
+ except (json.JSONDecodeError, TypeError):
232
+ decisions = []
233
+ if decisions:
234
+ block.append("Key decisions:")
235
+ for d in decisions[:4]:
236
+ block.append(f" - {d}")
237
+
238
+ # Artifacts
239
+ artifacts_raw = session.get("artifacts", "[]")
240
+ try:
241
+ artifacts = json.loads(artifacts_raw) if isinstance(artifacts_raw, str) else (artifacts_raw or [])
242
+ except (json.JSONDecodeError, TypeError):
243
+ artifacts = []
244
+ if artifacts:
245
+ block.append("Artifacts: " + ", ".join(artifacts[:5]))
246
+
247
+ block.append("")
248
+
249
+ block_chars = sum(len(l) for l in block)
250
+ if total_chars + block_chars > MAX_CONTEXT_CHARS and i > 1:
251
+ # Soft cap reached -- stop adding more sessions
252
+ break
253
+
254
+ lines.extend(block)
255
+ total_chars += block_chars
256
+
257
+ lines.append("---")
258
+ lines.append("Use this context to avoid re-asking questions or repeating work from prior sessions.")
259
+ lines.append("If a prior session is directly relevant, query LoreConvo MCP tools for full details.")
260
+
261
+ return "\n".join(lines)
262
+
263
+
264
+ def main():
265
+ """Main entry point for SessionStart hook."""
266
+ try:
267
+ stdin_data = sys.stdin.read()
268
+ if not stdin_data:
269
+ sys.exit(0)
270
+
271
+ hook_input = json.loads(stdin_data)
272
+ session_id = hook_input.get("session_id", "unknown")
273
+ cwd = hook_input.get("cwd", "")
274
+
275
+ db_path = get_db_path()
276
+
277
+ days_back = int(os.environ.get("LORECONVO_DAYS_BACK", "14"))
278
+ limit = int(os.environ.get("LORECONVO_LIMIT", "10"))
279
+ max_count = int(os.environ.get("LORECONVO_MAX_SESSIONS", "5"))
280
+
281
+ raw_sessions = query_recent_sessions(db_path, cwd, days_back=days_back, limit=limit)
282
+
283
+ if not raw_sessions:
284
+ sys.stderr.write(
285
+ f"LoreConvo auto-load: No recent sessions found for {cwd or 'any project'}\n"
286
+ )
287
+ sys.exit(0)
288
+
289
+ sessions = select_sessions(raw_sessions, max_count=max_count)
290
+
291
+ context = format_context(sessions, cwd)
292
+ if context:
293
+ print(context)
294
+ sys.stderr.write(
295
+ f"LoreConvo auto-load: Injected context from {len(sessions)} session(s) "
296
+ f"(scored from {len(raw_sessions)} candidates) for session {session_id}\n"
297
+ )
298
+
299
+ except json.JSONDecodeError:
300
+ sys.exit(0)
301
+ except Exception as e:
302
+ sys.stderr.write(f"LoreConvo auto-load error: {e}\n")
303
+ sys.exit(0)
304
+
305
+
306
+ if __name__ == "__main__":
307
+ main()
@@ -0,0 +1,301 @@
1
+ """LoreConvo SessionEnd auto-save hook.
2
+
3
+ Receives session metadata via stdin JSON from Claude Code's SessionEnd hook.
4
+ Parses the transcript JSONL to extract a summary, then saves directly to SQLite.
5
+
6
+ Designed to run within the 3-5 second timeout window.
7
+ """
8
+
9
+ import json
10
+ import os
11
+ import sys
12
+ import sqlite3
13
+ import uuid
14
+ from datetime import datetime
15
+ from pathlib import Path
16
+
17
+
18
+ def get_db_path():
19
+ """Get database path, matching core/config.py logic."""
20
+ return os.environ.get("LORECONVO_DB", os.path.expanduser("~/.loreconvo/sessions.db"))
21
+
22
+
23
+ def parse_transcript(transcript_path):
24
+ """Parse a Claude Code JSONL transcript into structured session data.
25
+
26
+ Extracts: title (from first user message), surface, summary of key exchanges,
27
+ decisions (lines starting with decision-like language), and artifacts.
28
+ """
29
+ if not transcript_path or not os.path.exists(transcript_path):
30
+ return None
31
+
32
+ messages = []
33
+ try:
34
+ with open(transcript_path, "r") as f:
35
+ for line in f:
36
+ line = line.strip()
37
+ if not line:
38
+ continue
39
+ try:
40
+ entry = json.loads(line)
41
+ messages.append(entry)
42
+ except json.JSONDecodeError:
43
+ continue
44
+ except (IOError, PermissionError):
45
+ return None
46
+
47
+ if not messages:
48
+ return None
49
+
50
+ # Extract user and assistant messages
51
+ user_messages = []
52
+ assistant_messages = []
53
+ tool_uses = []
54
+
55
+ for msg in messages:
56
+ # Real Claude Code transcripts wrap messages: {"type":"user", "message": {"role":..., "content":...}}
57
+ inner = msg.get("message", msg)
58
+ role = inner.get("role", "") if isinstance(inner, dict) else msg.get("role", "")
59
+ content = inner.get("content", "") if isinstance(inner, dict) else msg.get("content", "")
60
+
61
+ # Handle content that's a list of blocks
62
+ if isinstance(content, list):
63
+ text_parts = []
64
+ for block in content:
65
+ if isinstance(block, dict):
66
+ if block.get("type") == "text":
67
+ text_parts.append(block.get("text", ""))
68
+ elif block.get("type") == "tool_use":
69
+ tool_name = block.get("name", "unknown")
70
+ if tool_name == "Skill":
71
+ # Extract the actual skill name from the input parameter
72
+ # so skill-history shows e.g. "skill:langgraph-finance-workflow"
73
+ # instead of the raw string "Skill"
74
+ input_val = block.get("input") or {}
75
+ skill_name = input_val.get("skill") if isinstance(input_val, dict) else None
76
+ if skill_name:
77
+ tool_uses.append(f"skill:{skill_name}")
78
+ else:
79
+ tool_uses.append("Skill")
80
+ else:
81
+ tool_uses.append(tool_name)
82
+ elif isinstance(block, str):
83
+ text_parts.append(block)
84
+ content = " ".join(text_parts)
85
+
86
+ if role == "user" and content:
87
+ user_messages.append(content[:500]) # Truncate long messages
88
+ elif role == "assistant" and content:
89
+ assistant_messages.append(content[:500])
90
+
91
+ if not user_messages:
92
+ return None
93
+
94
+ # Title: first user message, truncated
95
+ first_msg = user_messages[0]
96
+ title = first_msg[:80].replace("\n", " ").strip()
97
+ if len(first_msg) > 80:
98
+ title += "..."
99
+
100
+ # Summary: combine first few exchanges
101
+ summary_parts = []
102
+ for i, msg in enumerate(user_messages[:3]):
103
+ summary_parts.append(f"User: {msg[:200]}")
104
+ if i < len(assistant_messages):
105
+ summary_parts.append(f"Assistant: {assistant_messages[i][:200]}")
106
+ summary = "\n".join(summary_parts)
107
+
108
+ # Truncate summary to reasonable length
109
+ if len(summary) > 2000:
110
+ summary = summary[:2000] + "..."
111
+
112
+ # Detect decisions (simple heuristic)
113
+ decisions = []
114
+ decision_keywords = ["decided", "agreed", "confirmed", "chose", "will use", "going with", "settled on"]
115
+ for msg in assistant_messages:
116
+ msg_lower = msg.lower()
117
+ for keyword in decision_keywords:
118
+ if keyword in msg_lower:
119
+ # Extract the sentence containing the keyword
120
+ for sentence in msg.split("."):
121
+ if keyword in sentence.lower():
122
+ clean = sentence.strip()
123
+ if clean and len(clean) > 10:
124
+ decisions.append(clean[:200])
125
+ break
126
+
127
+ # Detect artifacts (file paths, URLs)
128
+ artifacts = []
129
+ for msg in assistant_messages:
130
+ # Look for file paths
131
+ for word in msg.split():
132
+ if "/" in word and ("." in word.split("/")[-1]) and len(word) > 5:
133
+ clean = word.strip("(),\"'`")
134
+ if clean not in artifacts and len(artifacts) < 10:
135
+ artifacts.append(clean)
136
+
137
+ # Unique tools used
138
+ unique_tools = list(set(tool_uses))[:20]
139
+
140
+ return {
141
+ "title": title,
142
+ "summary": summary,
143
+ "decisions": decisions[:10],
144
+ "artifacts": artifacts[:10],
145
+ "tools_used": unique_tools,
146
+ "message_count": len(user_messages) + len(assistant_messages),
147
+ }
148
+
149
+
150
+ def ensure_tables(conn):
151
+ """Create tables if they don't exist (matches core/database.py schema)."""
152
+ conn.executescript("""
153
+ CREATE TABLE IF NOT EXISTS sessions (
154
+ id TEXT PRIMARY KEY NOT NULL,
155
+ title TEXT NOT NULL,
156
+ surface TEXT NOT NULL,
157
+ project TEXT,
158
+ summary TEXT,
159
+ decisions TEXT DEFAULT '[]',
160
+ artifacts TEXT DEFAULT '[]',
161
+ open_questions TEXT DEFAULT '[]',
162
+ tags TEXT DEFAULT '[]',
163
+ start_date TEXT,
164
+ end_date TEXT,
165
+ created_at TEXT DEFAULT (datetime('now')),
166
+ updated_at TEXT DEFAULT (datetime('now'))
167
+ );
168
+ CREATE TABLE IF NOT EXISTS session_skills (
169
+ session_id TEXT NOT NULL,
170
+ skill_name TEXT NOT NULL,
171
+ FOREIGN KEY (session_id) REFERENCES sessions(id),
172
+ UNIQUE(session_id, skill_name)
173
+ );
174
+ CREATE VIRTUAL TABLE IF NOT EXISTS sessions_fts USING fts5(
175
+ title, summary, decisions, content=sessions, content_rowid=rowid
176
+ );
177
+ """)
178
+
179
+
180
+ def save_to_db(db_path, session_id, parsed):
181
+ """Save parsed session data directly to SQLite."""
182
+ os.makedirs(os.path.dirname(db_path), exist_ok=True)
183
+
184
+ conn = sqlite3.connect(db_path)
185
+ try:
186
+ ensure_tables(conn)
187
+
188
+ # Use Claude's session_id as our primary key so dedup actually works.
189
+ # Previous bug: generated a random UUID for id but checked WHERE id = session_id,
190
+ # so the duplicate guard never matched anything.
191
+ session_uuid = session_id
192
+
193
+ # Check if session already exists (e.g., resumed session or duplicate hook fire)
194
+ cursor = conn.execute("SELECT id FROM sessions WHERE id = ?", (session_uuid,))
195
+ if cursor.fetchone():
196
+ # Already saved -- update instead of duplicate
197
+ now = datetime.now().isoformat()
198
+ conn.execute(
199
+ """UPDATE sessions SET summary = ?, decisions = ?, artifacts = ?,
200
+ tags = ?, end_date = ?, updated_at = ?
201
+ WHERE id = ?""",
202
+ (
203
+ parsed["summary"],
204
+ json.dumps(parsed["decisions"]),
205
+ json.dumps(parsed["artifacts"]),
206
+ json.dumps(["auto-saved"]),
207
+ now,
208
+ now,
209
+ session_uuid,
210
+ ),
211
+ )
212
+ conn.commit()
213
+ return True # Updated existing record
214
+
215
+ now = datetime.now().isoformat()
216
+
217
+ conn.execute(
218
+ """INSERT INTO sessions (id, title, surface, summary, decisions, artifacts,
219
+ open_questions, tags, start_date, end_date)
220
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
221
+ (
222
+ session_uuid,
223
+ parsed["title"],
224
+ "code",
225
+ parsed["summary"],
226
+ json.dumps(parsed["decisions"]),
227
+ json.dumps(parsed["artifacts"]),
228
+ json.dumps([]),
229
+ json.dumps(["auto-saved"]),
230
+ now,
231
+ now,
232
+ ),
233
+ )
234
+
235
+ # Update FTS index
236
+ try:
237
+ conn.execute(
238
+ """INSERT INTO sessions_fts(rowid, title, summary, decisions)
239
+ SELECT rowid, title, summary, decisions
240
+ FROM sessions WHERE id = ?""",
241
+ (session_uuid,),
242
+ )
243
+ except sqlite3.OperationalError:
244
+ pass # FTS table might not exist in older DBs
245
+
246
+ # Save skill/tool usage
247
+ for tool in parsed.get("tools_used", []):
248
+ try:
249
+ conn.execute(
250
+ "INSERT INTO session_skills (session_id, skill_name) VALUES (?, ?)",
251
+ (session_uuid, tool),
252
+ )
253
+ except sqlite3.IntegrityError:
254
+ pass
255
+
256
+ conn.commit()
257
+ return True
258
+ except Exception as e:
259
+ sys.stderr.write(f"LoreConvo auto-save DB error: {e}\n")
260
+ return False
261
+ finally:
262
+ conn.close()
263
+
264
+
265
+ def main():
266
+ """Main entry point for SessionEnd hook."""
267
+ try:
268
+ # Read hook input from stdin
269
+ stdin_data = sys.stdin.read()
270
+ if not stdin_data:
271
+ sys.exit(0)
272
+
273
+ hook_input = json.loads(stdin_data)
274
+ session_id = hook_input.get("session_id", "unknown")
275
+ transcript_path = hook_input.get("transcript_path", "")
276
+
277
+ # Parse transcript
278
+ parsed = parse_transcript(transcript_path)
279
+ if not parsed:
280
+ sys.exit(0)
281
+
282
+ # Skip very short sessions (less than 2 messages)
283
+ if parsed["message_count"] < 2:
284
+ sys.exit(0)
285
+
286
+ # Save to database
287
+ db_path = get_db_path()
288
+ saved = save_to_db(db_path, session_id, parsed)
289
+
290
+ if saved:
291
+ sys.stderr.write(f"LoreConvo: Auto-saved session '{parsed['title']}'\n")
292
+
293
+ except json.JSONDecodeError:
294
+ sys.exit(0)
295
+ except Exception as e:
296
+ sys.stderr.write(f"LoreConvo auto-save error: {e}\n")
297
+ sys.exit(0)
298
+
299
+
300
+ if __name__ == "__main__":
301
+ main()
@@ -0,0 +1,93 @@
1
+ Business Source License 1.1
2
+
3
+ Parameters
4
+
5
+ Licensor: Labyrinth Analytics Consulting
6
+ Licensed Work: LoreConvo v0.3.0
7
+ The Licensed Work is (c) 2026 Labyrinth Analytics Consulting
8
+ Additional Use Grant: You may make personal, non-commercial use of the Licensed Work,
9
+ subject to the following limitation: you may store no more than
10
+ 50 sessions in total across all databases on your machine. Any
11
+ use that exceeds this limit, or any commercial use, requires a
12
+ paid license from the Licensor.
13
+ Change Date: 2030-03-31
14
+ Change License: Apache License, Version 2.0
15
+
16
+ For information about alternative licensing arrangements for the Licensed Work,
17
+ please contact: info@labyrinthanalyticsconsulting.com
18
+
19
+ ---
20
+
21
+ Business Source License 1.1
22
+
23
+ License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
24
+ "Business Source License" is a trademark of MariaDB Corporation Ab.
25
+
26
+ Terms
27
+
28
+ The Licensor hereby grants you the right to copy, modify, create derivative
29
+ works, redistribute, and make non-production use of the Licensed Work. The
30
+ Licensor may make an Additional Use Grant, above, permitting limited production
31
+ use.
32
+
33
+ Effective on the Change Date, or the fourth anniversary of the first publicly
34
+ available distribution of a specific version of the Licensed Work under this
35
+ License, whichever comes first, the Licensor hereby grants you rights under
36
+ the terms of the Change License, and the rights granted in the paragraph
37
+ above terminate.
38
+
39
+ If your use of the Licensed Work does not comply with the requirements
40
+ currently in effect as described in this License, you must purchase a
41
+ commercial license from the Licensor, its affiliated entities, or authorized
42
+ resellers, or you must refrain from using the Licensed Work.
43
+
44
+ All copies of the original and modified Licensed Work, and derivative works
45
+ of the Licensed Work, are subject to this License. This License applies
46
+ separately for each version of the Licensed Work and the Change Date may vary
47
+ for each version of the Licensed Work released by Licensor.
48
+
49
+ You must conspicuously display this License on each original or modified copy
50
+ of the Licensed Work. If you receive a copy of the Licensed Work in
51
+ combination with other programs, as part of a larger work, or packaged by a
52
+ third party, and you receive the Licensed Work under a different license from
53
+ the one described in this License, you are required to comply with the license
54
+ applicable to you.
55
+
56
+ Any use of the Licensed Work in violation of this License will automatically
57
+ terminate your rights under this License for the current and all other
58
+ versions of the Licensed Work.
59
+
60
+ This License does not grant you any right in any trademark or logo of
61
+ Licensor or its affiliates (provided that you may use a trademark or logo of
62
+ Licensor as expressly required by this License).
63
+
64
+ TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
65
+ AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
66
+ EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
67
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
68
+ TITLE.
69
+
70
+ MariaDB hereby grants you permission to use this License's text to license
71
+ your works, and to refer to it using the trademark "Business Source License",
72
+ as long as you comply with the Covenants of Licensor below.
73
+
74
+ Covenants of Licensor
75
+
76
+ In consideration of the right to use this License's text and the "Business
77
+ Source License" name and trademark, Licensor covenants to MariaDB, and to all
78
+ other recipients of the licensed work to be provided under this License:
79
+
80
+ 1. To specify as the Change License the GPL Version 2.0 or any later version,
81
+ or a license that is compatible with GPL Version 2.0 or a later version,
82
+ where "compatible" means that software provided under the Change License can
83
+ be included in a program with software provided under GPL Version 2.0 or a
84
+ later version. Licensor may specify additional Change Licenses without
85
+ limitation.
86
+
87
+ 2. To either: (a) specify an additional grant of rights to use that does not
88
+ impose any additional restriction on the right granted in this License, as
89
+ the Additional Use Grant; or (b) insert the text "None".
90
+
91
+ 3. To specify a Change Date.
92
+
93
+ 4. Not to modify this License in any other way.