sessionreel 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.
- sessionreel/__init__.py +7 -0
- sessionreel/board.py +101 -0
- sessionreel/cli.py +193 -0
- sessionreel/demo/demo-session.jsonl +25 -0
- sessionreel/fonts/Inter-Regular.ttf +0 -0
- sessionreel/fonts/Inter-SemiBold.ttf +0 -0
- sessionreel/fonts/InterDisplay-Bold.ttf +0 -0
- sessionreel/fonts/JetBrainsMono-Bold.ttf +0 -0
- sessionreel/fonts/JetBrainsMono-Regular.ttf +0 -0
- sessionreel/fonts/NotoEmoji.ttf +0 -0
- sessionreel/fonts/OFL-Inter.txt +92 -0
- sessionreel/fonts/OFL-JetBrainsMono.txt +93 -0
- sessionreel/fonts/OFL-NotoEmoji.txt +93 -0
- sessionreel/fonts/OFL-Pretendard.txt +94 -0
- sessionreel/fonts/Pretendard-Bold.otf +0 -0
- sessionreel/fonts/Pretendard-Regular.otf +0 -0
- sessionreel/ingest.py +237 -0
- sessionreel/models.py +79 -0
- sessionreel/redact.py +139 -0
- sessionreel/render.py +736 -0
- sessionreel/story.py +507 -0
- sessionreel/voice.py +74 -0
- sessionreel-0.1.0.dist-info/METADATA +164 -0
- sessionreel-0.1.0.dist-info/RECORD +27 -0
- sessionreel-0.1.0.dist-info/WHEEL +4 -0
- sessionreel-0.1.0.dist-info/entry_points.txt +2 -0
- sessionreel-0.1.0.dist-info/licenses/LICENSE +24 -0
sessionreel/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""sessionreel: turn a coding-agent session log into a short recap video."""
|
|
2
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
__version__ = version("sessionreel")
|
|
6
|
+
except PackageNotFoundError: # running from a source checkout
|
|
7
|
+
__version__ = "0.0.0+dev"
|
sessionreel/board.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Checks a storyboard must pass before any frame is drawn.
|
|
2
|
+
|
|
3
|
+
A storyboard is plain JSON that people and agents edit between `plan` and `render`, so the
|
|
4
|
+
renderer cannot trust it: `validate` normalises its shape (clear errors instead of tracebacks),
|
|
5
|
+
`reredact` runs the redactor over every string again (idempotent — placeholders do not re-match),
|
|
6
|
+
and `caption_claims` flags numbers in captions that the scene's own data does not contain.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import copy
|
|
11
|
+
import math
|
|
12
|
+
import re
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from .redact import Redactor
|
|
16
|
+
|
|
17
|
+
KINDS = {"title", "prompt", "montage", "terminal", "diff", "ship", "say", "stats", "end"}
|
|
18
|
+
MAX_SECONDS = 30.0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BoardError(ValueError):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _str(v: Any) -> str:
|
|
26
|
+
return "" if v is None else str(v)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def validate(board: Any) -> dict:
|
|
30
|
+
"""Return a cleaned copy of `board` or raise BoardError naming the first problem."""
|
|
31
|
+
if not isinstance(board, dict):
|
|
32
|
+
raise BoardError("storyboard must be a JSON object")
|
|
33
|
+
scenes = board.get("scenes")
|
|
34
|
+
if not isinstance(scenes, list):
|
|
35
|
+
raise BoardError("storyboard has no 'scenes' list")
|
|
36
|
+
out = copy.deepcopy(board)
|
|
37
|
+
clean: list[dict] = []
|
|
38
|
+
for i, sc in enumerate(scenes):
|
|
39
|
+
if not isinstance(sc, dict):
|
|
40
|
+
raise BoardError(f"scene {i} is not an object")
|
|
41
|
+
kind = sc.get("kind")
|
|
42
|
+
if kind not in KINDS:
|
|
43
|
+
raise BoardError(f"scene {i}: unknown kind {kind!r} (expected one of {', '.join(sorted(KINDS))})")
|
|
44
|
+
sc = dict(sc)
|
|
45
|
+
try:
|
|
46
|
+
secs = float(sc.get("seconds", 3))
|
|
47
|
+
except (TypeError, ValueError):
|
|
48
|
+
raise BoardError(f"scene {i} ({kind}): seconds must be a number, got {sc.get('seconds')!r}") from None
|
|
49
|
+
if not math.isfinite(secs) or secs <= 0:
|
|
50
|
+
raise BoardError(f"scene {i} ({kind}): seconds must be a positive number, got {secs!r}")
|
|
51
|
+
sc["seconds"] = min(secs, MAX_SECONDS)
|
|
52
|
+
for key in ("caption", "fact", "text", "command", "file", "message", "badge", "status",
|
|
53
|
+
"project", "title", "date", "model", "branch", "duration"):
|
|
54
|
+
if key in sc:
|
|
55
|
+
sc[key] = _str(sc[key])
|
|
56
|
+
for key in ("output", "lines", "files"):
|
|
57
|
+
if key in sc:
|
|
58
|
+
if not isinstance(sc[key], list):
|
|
59
|
+
raise BoardError(f"scene {i} ({kind}): {key} must be a list of strings")
|
|
60
|
+
sc[key] = [_str(x) for x in sc[key]]
|
|
61
|
+
if kind == "stats":
|
|
62
|
+
items = sc.get("items", [])
|
|
63
|
+
if not isinstance(items, list) or not all(isinstance(it, (list, tuple)) and len(it) == 2 for it in items):
|
|
64
|
+
raise BoardError(f"scene {i} (stats): items must be a list of [label, value] pairs")
|
|
65
|
+
sc["items"] = [[_str(a), _str(b)] for a, b in items]
|
|
66
|
+
for key in ("added", "removed"):
|
|
67
|
+
if key in sc:
|
|
68
|
+
try:
|
|
69
|
+
sc[key] = int(sc[key])
|
|
70
|
+
except (TypeError, ValueError):
|
|
71
|
+
raise BoardError(f"scene {i} ({kind}): {key} must be an integer") from None
|
|
72
|
+
clean.append(sc)
|
|
73
|
+
out["scenes"] = clean
|
|
74
|
+
return out
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def reredact(board: dict, redactor: Redactor | None = None) -> tuple[dict, dict[str, int]]:
|
|
78
|
+
"""Run redaction over every string in every scene. Returns the board and what changed."""
|
|
79
|
+
r = redactor or Redactor()
|
|
80
|
+
scenes = r.value(board["scenes"])
|
|
81
|
+
return {**board, "scenes": scenes}, dict(r.counts)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
_NUM = re.compile(r"\d[\d,.]*")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def caption_claims(board: dict) -> list[str]:
|
|
88
|
+
"""Numbers in a caption that appear nowhere in that scene's data or its template fact."""
|
|
89
|
+
warnings: list[str] = []
|
|
90
|
+
for i, sc in enumerate(board.get("scenes", [])):
|
|
91
|
+
cap = sc.get("caption", "")
|
|
92
|
+
if not cap or cap == sc.get("fact"):
|
|
93
|
+
continue
|
|
94
|
+
support = " ".join(_str(v) if not isinstance(v, list) else " ".join(map(_str, v))
|
|
95
|
+
for k, v in sc.items() if k != "caption")
|
|
96
|
+
known = {n.strip(".,").replace(",", "") for n in _NUM.findall(support)}
|
|
97
|
+
for n in _NUM.findall(cap):
|
|
98
|
+
n = n.strip(".,").replace(",", "")
|
|
99
|
+
if n and n not in known:
|
|
100
|
+
warnings.append(f"scene {i} ({sc.get('kind')}): caption says {n!r}, which is not in the scene data")
|
|
101
|
+
return warnings
|
sessionreel/cli.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""sessionreel command line."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from importlib import resources
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from . import __version__, ingest, redact, render, story
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _eprint(*a: object) -> None:
|
|
16
|
+
print(*a, file=sys.stderr)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _progress(done: int, total: int) -> None:
|
|
20
|
+
if done == total or done % 15 == 0:
|
|
21
|
+
bar = int(28 * done / max(1, total))
|
|
22
|
+
sys.stderr.write(f"\r rendering [{'█' * bar}{'·' * (28 - bar)}] {done}/{total} frames")
|
|
23
|
+
if done == total:
|
|
24
|
+
sys.stderr.write("\n")
|
|
25
|
+
sys.stderr.flush()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _resolve(ref: str | None) -> Path:
|
|
29
|
+
if ref:
|
|
30
|
+
return ingest.find_session(ref)
|
|
31
|
+
found = ingest.current_session()
|
|
32
|
+
if found is None:
|
|
33
|
+
raise FileNotFoundError(
|
|
34
|
+
f"no Claude Code sessions for {os.getcwd()} (or its parents) under {ingest.projects_root()}\n"
|
|
35
|
+
" run `sessionreel list` to see all sessions, or pass a session id / .jsonl path")
|
|
36
|
+
return found
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _plan(args: argparse.Namespace) -> dict:
|
|
40
|
+
path = _resolve(args.session)
|
|
41
|
+
sess = ingest.load(path)
|
|
42
|
+
redact.redact_session(sess, extra=args.redact or ())
|
|
43
|
+
board = story.build(sess, lang=args.lang, whole=args.whole,
|
|
44
|
+
project_name=getattr(args, "project", None), show_branch=not getattr(args, "no_branch", False))
|
|
45
|
+
n = sum(board["redactions"].values())
|
|
46
|
+
ask = next((s["text"] for s in board["scenes"] if s["kind"] == "prompt"), "")
|
|
47
|
+
what = ", ".join(f"{v} {k}" for k, v in sorted(board["redactions"].items(), key=lambda kv: -kv[1]))
|
|
48
|
+
_eprint(f" session {path.stem} ({len(sess.events)} events"
|
|
49
|
+
+ (f", {sess.skipped_lines} malformed lines skipped" if sess.skipped_lines else "") + ")")
|
|
50
|
+
if ask:
|
|
51
|
+
_eprint(f" ask {ask[:90]}{'…' if len(ask) > 90 else ''}")
|
|
52
|
+
_eprint(f" story {' → '.join(s['kind'] for s in board['scenes'])}"
|
|
53
|
+
+ ("" if board["arc"] else " (no red→green arc found; built from the largest edits)"))
|
|
54
|
+
_eprint(f" redacted {n} item{'s' if n != 1 else ''}" + (f" ({what})" if what else ""))
|
|
55
|
+
return board
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _render(board: dict, args: argparse.Namespace) -> Path:
|
|
59
|
+
from .board import caption_claims, reredact, validate
|
|
60
|
+
board = validate(board)
|
|
61
|
+
board, changed = reredact(board)
|
|
62
|
+
if changed:
|
|
63
|
+
_eprint(f" redacted {sum(changed.values())} more item(s) found in the storyboard ({', '.join(changed)})")
|
|
64
|
+
for w in caption_claims(board):
|
|
65
|
+
_eprint(f" warning {w}")
|
|
66
|
+
out = Path(args.output)
|
|
67
|
+
audio = None
|
|
68
|
+
if args.voice is not None:
|
|
69
|
+
from . import voice
|
|
70
|
+
board, audio = voice.narrate(board, voice=args.voice or None)
|
|
71
|
+
t0 = time.time()
|
|
72
|
+
render.render(board, out, fmt=args.format, audio=audio, progress=_progress)
|
|
73
|
+
secs = sum(float(s.get("seconds", 3)) for s in board["scenes"])
|
|
74
|
+
_eprint(f" wrote {out} ({secs:.0f}s video, {out.stat().st_size / 1e6:.1f} MB, {time.time() - t0:.0f}s to render)")
|
|
75
|
+
if args.gif:
|
|
76
|
+
gif = render.to_gif(out, out.with_suffix(".gif"))
|
|
77
|
+
_eprint(f" wrote {gif} ({gif.stat().st_size / 1e6:.1f} MB)")
|
|
78
|
+
return out
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def cmd_make(args: argparse.Namespace) -> int:
|
|
82
|
+
board = _plan(args)
|
|
83
|
+
_render(board, args)
|
|
84
|
+
return 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def cmd_plan(args: argparse.Namespace) -> int:
|
|
88
|
+
board = _plan(args)
|
|
89
|
+
text = json.dumps(board, ensure_ascii=False, indent=2)
|
|
90
|
+
if args.output == "-":
|
|
91
|
+
print(text)
|
|
92
|
+
else:
|
|
93
|
+
Path(args.output).write_text(text + "\n", encoding="utf-8")
|
|
94
|
+
_eprint(f" wrote {args.output} — edit captions, then: sessionreel render {args.output}")
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def cmd_render(args: argparse.Namespace) -> int:
|
|
99
|
+
board = json.loads(Path(args.storyboard).read_text(encoding="utf-8"))
|
|
100
|
+
if board.get("version") != story.STORYBOARD_VERSION:
|
|
101
|
+
_eprint(f" storyboard version {board.get('version')} is not supported by this sessionreel ({__version__})")
|
|
102
|
+
return 2
|
|
103
|
+
_render(board, args)
|
|
104
|
+
return 0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def cmd_list(args: argparse.Namespace) -> int:
|
|
108
|
+
files = ingest.session_files(cwd=os.getcwd() if args.here else None)[: args.limit]
|
|
109
|
+
if not files:
|
|
110
|
+
_eprint(f" no sessions under {ingest.projects_root()}")
|
|
111
|
+
return 1
|
|
112
|
+
home = str(Path.home())
|
|
113
|
+
for f in files:
|
|
114
|
+
sess = ingest.load(f)
|
|
115
|
+
when = time.strftime("%Y-%m-%d %H:%M", time.localtime(f.stat().st_mtime))
|
|
116
|
+
cwd = sess.cwd.replace(home, "~") if sess.cwd else "?"
|
|
117
|
+
mins = int(story.active_seconds(sess.events) // 60)
|
|
118
|
+
print(f"{f.stem[:8]} {when} {mins:>4}m {len(sess.prompts):>3} prompts {len(sess.tools):>4} tools {cwd}")
|
|
119
|
+
return 0
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def cmd_demo(args: argparse.Namespace) -> int:
|
|
123
|
+
args.session = str(resources.files("sessionreel").joinpath("demo", "demo-session.jsonl"))
|
|
124
|
+
return cmd_make(args)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
128
|
+
p = argparse.ArgumentParser(
|
|
129
|
+
prog="sessionreel",
|
|
130
|
+
description="Turn a Claude Code session into a short recap video. Local, redacted, no API key.",
|
|
131
|
+
)
|
|
132
|
+
p.add_argument("--version", action="version", version=f"sessionreel {__version__}")
|
|
133
|
+
sub = p.add_subparsers(dest="cmd")
|
|
134
|
+
|
|
135
|
+
def story_opts(sp: argparse.ArgumentParser) -> None:
|
|
136
|
+
sp.add_argument("session", nargs="?", help="session id, id prefix or .jsonl path (default: latest here)")
|
|
137
|
+
sp.add_argument("--lang", choices=sorted(story.T), default="en", help="caption language")
|
|
138
|
+
sp.add_argument("--whole", action="store_true", help="tell the whole session, not just the episode around the fix")
|
|
139
|
+
sp.add_argument("--redact", action="append", metavar="REGEX", help="extra pattern to redact (repeatable)")
|
|
140
|
+
sp.add_argument("--project", metavar="NAME", help="name shown on every frame instead of the directory name")
|
|
141
|
+
sp.add_argument("--no-branch", action="store_true", help="do not show the git branch")
|
|
142
|
+
|
|
143
|
+
def video_opts(sp: argparse.ArgumentParser, default_out: str = "reel.mp4") -> None:
|
|
144
|
+
sp.add_argument("-o", "--output", default=default_out)
|
|
145
|
+
sp.add_argument("--format", choices=sorted(render.FORMATS), default="square")
|
|
146
|
+
sp.add_argument("--voice", nargs="?", const="", default=None, metavar="NAME",
|
|
147
|
+
help="narrate captions with the OS voice (optionally a voice name)")
|
|
148
|
+
sp.add_argument("--gif", action="store_true", help="also write a GIF next to the mp4")
|
|
149
|
+
|
|
150
|
+
mk = sub.add_parser("make", help="session → mp4 (default command)")
|
|
151
|
+
story_opts(mk)
|
|
152
|
+
video_opts(mk)
|
|
153
|
+
mk.set_defaults(fn=cmd_make)
|
|
154
|
+
|
|
155
|
+
pl = sub.add_parser("plan", help="session → editable storyboard.json")
|
|
156
|
+
story_opts(pl)
|
|
157
|
+
pl.add_argument("-o", "--output", default="storyboard.json", help="path, or - for stdout")
|
|
158
|
+
pl.set_defaults(fn=cmd_plan)
|
|
159
|
+
|
|
160
|
+
rd = sub.add_parser("render", help="storyboard.json → mp4")
|
|
161
|
+
rd.add_argument("storyboard")
|
|
162
|
+
video_opts(rd)
|
|
163
|
+
rd.set_defaults(fn=cmd_render)
|
|
164
|
+
|
|
165
|
+
ls = sub.add_parser("list", help="recent sessions")
|
|
166
|
+
ls.add_argument("--here", action="store_true", help="only sessions for the current directory")
|
|
167
|
+
ls.add_argument("-n", "--limit", type=int, default=20)
|
|
168
|
+
ls.set_defaults(fn=cmd_list)
|
|
169
|
+
|
|
170
|
+
dm = sub.add_parser("demo", help="render the bundled sample session (no logs needed)")
|
|
171
|
+
dm.add_argument("--lang", choices=sorted(story.T), default="en")
|
|
172
|
+
video_opts(dm, "sessionreel-demo.mp4")
|
|
173
|
+
dm.set_defaults(fn=cmd_demo, whole=False, redact=None, project=None, no_branch=False)
|
|
174
|
+
return p
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def main(argv: list[str] | None = None) -> int:
|
|
178
|
+
argv = sys.argv[1:] if argv is None else argv
|
|
179
|
+
parser = build_parser()
|
|
180
|
+
if not argv or argv[0] not in {"make", "plan", "render", "list", "demo", "-h", "--help", "--version"}:
|
|
181
|
+
argv = ["make", *argv]
|
|
182
|
+
args = parser.parse_args(argv)
|
|
183
|
+
try:
|
|
184
|
+
return args.fn(args)
|
|
185
|
+
except (FileNotFoundError, ValueError, RuntimeError, OSError, json.JSONDecodeError) as e:
|
|
186
|
+
_eprint(f"sessionreel: {e}")
|
|
187
|
+
return 1
|
|
188
|
+
except KeyboardInterrupt:
|
|
189
|
+
return 130
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
if __name__ == "__main__":
|
|
193
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{"type": "ai-title", "aiTitle": "Unicode titles produce empty slugs", "sessionId": "5e55a0d0-de40-4c1e-9d3e-000000000001"}
|
|
2
|
+
{"parentUuid": null, "isSidechain": false, "userType": "external", "cwd": "/home/dev/slugkit", "sessionId": "5e55a0d0-de40-4c1e-9d3e-000000000001", "version": "2.1.270", "gitBranch": "fix/unicode-slugs", "type": "user", "uuid": "72acfd2f-7482-4fdc-a3f1-c2c764cc7afc", "timestamp": "2026-09-18T14:02:11Z", "message": {"role": "user", "content": "Titles with accents or emoji come out as broken slugs — \"Crème brûlée 🍮\" becomes \"cr-me-br-l-e\". Fix slugify so it produces \"creme-brulee\", cap slugs at 80 chars, and add tests."}}
|
|
3
|
+
{"parentUuid": "72acfd2f-7482-4fdc-a3f1-c2c764cc7afc", "isSidechain": false, "userType": "external", "cwd": "/home/dev/slugkit", "sessionId": "5e55a0d0-de40-4c1e-9d3e-000000000001", "version": "2.1.270", "gitBranch": "fix/unicode-slugs", "type": "assistant", "uuid": "db7b1b2f-7827-4ef8-908a-f85fdc9535db", "timestamp": "2026-09-18T14:02:15Z", "message": {"model": "claude-opus-5", "role": "assistant", "type": "message", "content": [{"type": "text", "text": "I'll look at how slugs are built and what the tests cover first."}], "usage": {"input_tokens": 6, "cache_creation_input_tokens": 2100, "cache_read_input_tokens": 31000, "output_tokens": 180}}}
|
|
4
|
+
{"parentUuid": "db7b1b2f-7827-4ef8-908a-f85fdc9535db", "isSidechain": false, "userType": "external", "cwd": "/home/dev/slugkit", "sessionId": "5e55a0d0-de40-4c1e-9d3e-000000000001", "version": "2.1.270", "gitBranch": "fix/unicode-slugs", "type": "assistant", "uuid": "5926dc4d-f3de-4731-b3f5-97611cdd5910", "timestamp": "2026-09-18T14:02:20Z", "message": {"model": "claude-opus-5", "role": "assistant", "type": "message", "content": [{"type": "tool_use", "id": "toolu_e4987968ed914b69b42e1f81", "name": "Read", "input": {"file_path": "/home/dev/slugkit/slugkit/slug.py"}}], "usage": {"input_tokens": 4, "cache_creation_input_tokens": 900, "cache_read_input_tokens": 33000, "output_tokens": 220}}}
|
|
5
|
+
{"parentUuid": "5926dc4d-f3de-4731-b3f5-97611cdd5910", "isSidechain": false, "userType": "external", "cwd": "/home/dev/slugkit", "sessionId": "5e55a0d0-de40-4c1e-9d3e-000000000001", "version": "2.1.270", "gitBranch": "fix/unicode-slugs", "type": "user", "uuid": "6310dfe0-48de-4a3b-9ada-02a0c2ae149f", "timestamp": "2026-09-18T14:02:22Z", "message": {"role": "user", "content": [{"tool_use_id": "toolu_e4987968ed914b69b42e1f81", "type": "tool_result", "content": "import re\n\n\ndef slugify(title: str) -> str:\n \"\"\"Lowercase, keep a-z0-9, join words with '-'.\"\"\"\n words = re.findall(r\"[a-z0-9]+\", title.lower())\n return \"-\".join(words)\n", "is_error": false}]}}
|
|
6
|
+
{"parentUuid": "6310dfe0-48de-4a3b-9ada-02a0c2ae149f", "isSidechain": false, "userType": "external", "cwd": "/home/dev/slugkit", "sessionId": "5e55a0d0-de40-4c1e-9d3e-000000000001", "version": "2.1.270", "gitBranch": "fix/unicode-slugs", "type": "assistant", "uuid": "8cf3b5ab-9691-4a02-8e5f-b5a5e155dde4", "timestamp": "2026-09-18T14:02:27Z", "message": {"model": "claude-opus-5", "role": "assistant", "type": "message", "content": [{"type": "tool_use", "id": "toolu_ee00f0f96c7445b78df7e9dd", "name": "Grep", "input": {"pattern": "slugify\\(", "path": "/home/dev/slugkit"}}], "usage": {"input_tokens": 4, "cache_creation_input_tokens": 900, "cache_read_input_tokens": 33000, "output_tokens": 220}}}
|
|
7
|
+
{"parentUuid": "8cf3b5ab-9691-4a02-8e5f-b5a5e155dde4", "isSidechain": false, "userType": "external", "cwd": "/home/dev/slugkit", "sessionId": "5e55a0d0-de40-4c1e-9d3e-000000000001", "version": "2.1.270", "gitBranch": "fix/unicode-slugs", "type": "user", "uuid": "972acd37-17ce-47e9-aac8-8e50230e96c3", "timestamp": "2026-09-18T14:02:29Z", "message": {"role": "user", "content": [{"tool_use_id": "toolu_ee00f0f96c7445b78df7e9dd", "type": "tool_result", "content": "slugkit/slug.py\nslugkit/posts.py\nslugkit/feed.py\ntests/test_slug.py", "is_error": false}]}}
|
|
8
|
+
{"parentUuid": "972acd37-17ce-47e9-aac8-8e50230e96c3", "isSidechain": false, "userType": "external", "cwd": "/home/dev/slugkit", "sessionId": "5e55a0d0-de40-4c1e-9d3e-000000000001", "version": "2.1.270", "gitBranch": "fix/unicode-slugs", "type": "assistant", "uuid": "4ef38ca9-3398-42d2-941a-0f546d0309db", "timestamp": "2026-09-18T14:02:34Z", "message": {"model": "claude-opus-5", "role": "assistant", "type": "message", "content": [{"type": "tool_use", "id": "toolu_1eeabb0fc4e344129403c1dc", "name": "Read", "input": {"file_path": "/home/dev/slugkit/slugkit/posts.py"}}], "usage": {"input_tokens": 4, "cache_creation_input_tokens": 900, "cache_read_input_tokens": 33000, "output_tokens": 220}}}
|
|
9
|
+
{"parentUuid": "4ef38ca9-3398-42d2-941a-0f546d0309db", "isSidechain": false, "userType": "external", "cwd": "/home/dev/slugkit", "sessionId": "5e55a0d0-de40-4c1e-9d3e-000000000001", "version": "2.1.270", "gitBranch": "fix/unicode-slugs", "type": "user", "uuid": "4628f880-f74a-4792-bc63-d8ba78f623a9", "timestamp": "2026-09-18T14:02:36Z", "message": {"role": "user", "content": [{"tool_use_id": "toolu_1eeabb0fc4e344129403c1dc", "type": "tool_result", "content": "from .slug import slugify\n...", "is_error": false}]}}
|
|
10
|
+
{"parentUuid": "4628f880-f74a-4792-bc63-d8ba78f623a9", "isSidechain": false, "userType": "external", "cwd": "/home/dev/slugkit", "sessionId": "5e55a0d0-de40-4c1e-9d3e-000000000001", "version": "2.1.270", "gitBranch": "fix/unicode-slugs", "type": "assistant", "uuid": "0cd11491-21df-488b-bf48-379f99069811", "timestamp": "2026-09-18T14:02:41Z", "message": {"model": "claude-opus-5", "role": "assistant", "type": "message", "content": [{"type": "tool_use", "id": "toolu_120cce50341b4d16bf5bb118", "name": "Read", "input": {"file_path": "/home/dev/slugkit/tests/test_slug.py"}}], "usage": {"input_tokens": 4, "cache_creation_input_tokens": 900, "cache_read_input_tokens": 33000, "output_tokens": 220}}}
|
|
11
|
+
{"parentUuid": "0cd11491-21df-488b-bf48-379f99069811", "isSidechain": false, "userType": "external", "cwd": "/home/dev/slugkit", "sessionId": "5e55a0d0-de40-4c1e-9d3e-000000000001", "version": "2.1.270", "gitBranch": "fix/unicode-slugs", "type": "user", "uuid": "5f19650e-1e3f-4ba3-ad36-fe20a7482f6d", "timestamp": "2026-09-18T14:02:43Z", "message": {"role": "user", "content": [{"tool_use_id": "toolu_120cce50341b4d16bf5bb118", "type": "tool_result", "content": "def test_basic(): ...", "is_error": false}]}}
|
|
12
|
+
{"parentUuid": "5f19650e-1e3f-4ba3-ad36-fe20a7482f6d", "isSidechain": false, "userType": "external", "cwd": "/home/dev/slugkit", "sessionId": "5e55a0d0-de40-4c1e-9d3e-000000000001", "version": "2.1.270", "gitBranch": "fix/unicode-slugs", "type": "assistant", "uuid": "d8e21392-7966-4f56-972f-7ad755a267cb", "timestamp": "2026-09-18T14:02:48Z", "message": {"model": "claude-opus-5", "role": "assistant", "type": "message", "content": [{"type": "tool_use", "id": "toolu_e9ddc3d9bd144b919c460c96", "name": "Read", "input": {"file_path": "/home/dev/slugkit/pyproject.toml"}}], "usage": {"input_tokens": 4, "cache_creation_input_tokens": 900, "cache_read_input_tokens": 33000, "output_tokens": 220}}}
|
|
13
|
+
{"parentUuid": "d8e21392-7966-4f56-972f-7ad755a267cb", "isSidechain": false, "userType": "external", "cwd": "/home/dev/slugkit", "sessionId": "5e55a0d0-de40-4c1e-9d3e-000000000001", "version": "2.1.270", "gitBranch": "fix/unicode-slugs", "type": "user", "uuid": "318366d2-a5e9-47d2-802d-c7fad6e7c391", "timestamp": "2026-09-18T14:02:50Z", "message": {"role": "user", "content": [{"tool_use_id": "toolu_e9ddc3d9bd144b919c460c96", "type": "tool_result", "content": "[project]\nname = \"slugkit\"", "is_error": false}]}}
|
|
14
|
+
{"parentUuid": "318366d2-a5e9-47d2-802d-c7fad6e7c391", "isSidechain": false, "userType": "external", "cwd": "/home/dev/slugkit", "sessionId": "5e55a0d0-de40-4c1e-9d3e-000000000001", "version": "2.1.270", "gitBranch": "fix/unicode-slugs", "type": "assistant", "uuid": "3cf5b1fe-4c72-4e7f-a34a-e40bf435141b", "timestamp": "2026-09-18T14:02:59Z", "message": {"model": "claude-opus-5", "role": "assistant", "type": "message", "content": [{"type": "tool_use", "id": "toolu_537f9b8411a24b07a132a244", "name": "Edit", "input": {"file_path": "/home/dev/slugkit/tests/test_slug.py", "old_string": "def test_basic():", "new_string": "def test_accents():\n assert slugify(\"Crème brûlée 🍮\") == \"creme-brulee\"\n\n\ndef test_max_len():\n assert len(slugify(\"word \" * 40)) <= 80\n\n\ndef test_basic():"}}], "usage": {"input_tokens": 4, "cache_creation_input_tokens": 900, "cache_read_input_tokens": 33000, "output_tokens": 220}}}
|
|
15
|
+
{"parentUuid": "3cf5b1fe-4c72-4e7f-a34a-e40bf435141b", "isSidechain": false, "userType": "external", "cwd": "/home/dev/slugkit", "sessionId": "5e55a0d0-de40-4c1e-9d3e-000000000001", "version": "2.1.270", "gitBranch": "fix/unicode-slugs", "type": "user", "uuid": "b02e5ee3-a7e3-4e43-854b-f52fa3965b44", "timestamp": "2026-09-18T14:03:01Z", "message": {"role": "user", "content": [{"tool_use_id": "toolu_537f9b8411a24b07a132a244", "type": "tool_result", "content": "The file tests/test_slug.py has been updated.", "is_error": false}]}, "toolUseResult": {"filePath": "/home/dev/slugkit/tests/test_slug.py", "structuredPatch": [{"oldStart": 1, "oldLines": 3, "newStart": 1, "newLines": 11, "lines": [" from slugkit.slug import slugify", "+", "+", "+def test_accents():", "+ assert slugify(\"Crème brûlée 🍮\") == \"creme-brulee\"", "+", "+", "+def test_max_len():", "+ assert len(slugify(\"word \" * 40)) <= 80", " ", " "]}]}}
|
|
16
|
+
{"parentUuid": "b02e5ee3-a7e3-4e43-854b-f52fa3965b44", "isSidechain": false, "userType": "external", "cwd": "/home/dev/slugkit", "sessionId": "5e55a0d0-de40-4c1e-9d3e-000000000001", "version": "2.1.270", "gitBranch": "fix/unicode-slugs", "type": "assistant", "uuid": "bbead3f9-a6c8-43b9-98ef-806d2d2580f4", "timestamp": "2026-09-18T14:03:15Z", "message": {"model": "claude-opus-5", "role": "assistant", "type": "message", "content": [{"type": "tool_use", "id": "toolu_f575db7dcbef41228716d19f", "name": "Bash", "input": {"command": "cd /home/dev/slugkit && python -m pytest -q", "description": "Run tests"}}], "usage": {"input_tokens": 4, "cache_creation_input_tokens": 900, "cache_read_input_tokens": 33000, "output_tokens": 220}}}
|
|
17
|
+
{"parentUuid": "bbead3f9-a6c8-43b9-98ef-806d2d2580f4", "isSidechain": false, "userType": "external", "cwd": "/home/dev/slugkit", "sessionId": "5e55a0d0-de40-4c1e-9d3e-000000000001", "version": "2.1.270", "gitBranch": "fix/unicode-slugs", "type": "user", "uuid": "17cb27ba-2866-4a3b-9be4-21320117bc30", "timestamp": "2026-09-18T14:03:17Z", "message": {"role": "user", "content": [{"tool_use_id": "toolu_f575db7dcbef41228716d19f", "type": "tool_result", "content": "Exit code 1\n..F.F......... [100%]\n=================================== FAILURES ===================================\n________________________________ test_accents _________________________________\n> assert slugify(\"Crème brûlée 🍮\") == \"creme-brulee\"\nE AssertionError: assert 'cr-me-br-l-e' == 'creme-brulee'\n________________________________ test_max_len _________________________________\nE assert 199 <= 80\n=========================== short test summary info ============================\nFAILED tests/test_slug.py::test_accents - AssertionError\nFAILED tests/test_slug.py::test_max_len - assert 199 <= 80\n2 failed, 12 passed in 0.31s", "is_error": true}]}}
|
|
18
|
+
{"parentUuid": "17cb27ba-2866-4a3b-9be4-21320117bc30", "isSidechain": false, "userType": "external", "cwd": "/home/dev/slugkit", "sessionId": "5e55a0d0-de40-4c1e-9d3e-000000000001", "version": "2.1.270", "gitBranch": "fix/unicode-slugs", "type": "assistant", "uuid": "8b9fa0cf-2791-47be-b6e2-41e59fba9621", "timestamp": "2026-09-18T14:03:21Z", "message": {"model": "claude-opus-5", "role": "assistant", "type": "message", "content": [{"type": "text", "text": "Two failures, as expected: accented letters are dropped instead of folded, and there is no length cap. Normalising to NFKD and encoding to ASCII folds é→e and drops the emoji."}], "usage": {"input_tokens": 6, "cache_creation_input_tokens": 2100, "cache_read_input_tokens": 31000, "output_tokens": 180}}}
|
|
19
|
+
{"parentUuid": "8b9fa0cf-2791-47be-b6e2-41e59fba9621", "isSidechain": false, "userType": "external", "cwd": "/home/dev/slugkit", "sessionId": "5e55a0d0-de40-4c1e-9d3e-000000000001", "version": "2.1.270", "gitBranch": "fix/unicode-slugs", "type": "assistant", "uuid": "ef99cae3-cd36-4292-9925-dbcd8b9ef4a1", "timestamp": "2026-09-18T14:03:42Z", "message": {"model": "claude-opus-5", "role": "assistant", "type": "message", "content": [{"type": "tool_use", "id": "toolu_e3e03248803c49ab9521cf24", "name": "Edit", "input": {"file_path": "/home/dev/slugkit/slugkit/slug.py", "old_string": "import re\n\n\ndef slugify(title: str) -> str:\n \"\"\"Lowercase, keep a-z0-9, join words with '-'.\"\"\"\n words = re.findall(r\"[a-z0-9]+\", title.lower())\n return \"-\".join(words)\n", "new_string": "import re\nimport unicodedata\n\n\ndef slugify(title: str, max_len: int = 80) -> str:\n \"\"\"ASCII slug: strip accents, drop symbols and emoji, join words with '-'.\"\"\"\n text = unicodedata.normalize(\"NFKD\", title)\n text = text.encode(\"ascii\", \"ignore\").decode(\"ascii\")\n words = re.findall(r\"[a-z0-9]+\", text.lower())\n return \"-\".join(words)[:max_len].rstrip(\"-\")\n"}}], "usage": {"input_tokens": 4, "cache_creation_input_tokens": 900, "cache_read_input_tokens": 33000, "output_tokens": 220}}}
|
|
20
|
+
{"parentUuid": "ef99cae3-cd36-4292-9925-dbcd8b9ef4a1", "isSidechain": false, "userType": "external", "cwd": "/home/dev/slugkit", "sessionId": "5e55a0d0-de40-4c1e-9d3e-000000000001", "version": "2.1.270", "gitBranch": "fix/unicode-slugs", "type": "user", "uuid": "0deb9e41-898a-46ae-b721-a5c4325f9063", "timestamp": "2026-09-18T14:03:44Z", "message": {"role": "user", "content": [{"tool_use_id": "toolu_e3e03248803c49ab9521cf24", "type": "tool_result", "content": "The file slugkit/slug.py has been updated.", "is_error": false}]}, "toolUseResult": {"filePath": "/home/dev/slugkit/slugkit/slug.py", "structuredPatch": [{"oldStart": 1, "oldLines": 8, "newStart": 1, "newLines": 11, "lines": [" import re", "+import unicodedata", " ", " ", "-def slugify(title: str) -> str:", "- \"\"\"Lowercase, keep a-z0-9, join words with '-'.\"\"\"", "- words = re.findall(r\"[a-z0-9]+\", title.lower())", "- return \"-\".join(words)", "+def slugify(title: str, max_len: int = 80) -> str:", "+ \"\"\"ASCII slug: strip accents, drop symbols and emoji, join words with '-'.\"\"\"", "+ text = unicodedata.normalize(\"NFKD\", title)", "+ text = text.encode(\"ascii\", \"ignore\").decode(\"ascii\")", "+ words = re.findall(r\"[a-z0-9]+\", text.lower())", "+ return \"-\".join(words)[:max_len].rstrip(\"-\")", " "]}]}}
|
|
21
|
+
{"parentUuid": "0deb9e41-898a-46ae-b721-a5c4325f9063", "isSidechain": false, "userType": "external", "cwd": "/home/dev/slugkit", "sessionId": "5e55a0d0-de40-4c1e-9d3e-000000000001", "version": "2.1.270", "gitBranch": "fix/unicode-slugs", "type": "assistant", "uuid": "2755f14f-5fe0-4396-9edb-90b8c4ed30f5", "timestamp": "2026-09-18T14:03:56Z", "message": {"model": "claude-opus-5", "role": "assistant", "type": "message", "content": [{"type": "tool_use", "id": "toolu_867fd826f46a4126ac85342b", "name": "Bash", "input": {"command": "cd /home/dev/slugkit && python -m pytest -q", "description": "Run tests"}}], "usage": {"input_tokens": 4, "cache_creation_input_tokens": 900, "cache_read_input_tokens": 33000, "output_tokens": 220}}}
|
|
22
|
+
{"parentUuid": "2755f14f-5fe0-4396-9edb-90b8c4ed30f5", "isSidechain": false, "userType": "external", "cwd": "/home/dev/slugkit", "sessionId": "5e55a0d0-de40-4c1e-9d3e-000000000001", "version": "2.1.270", "gitBranch": "fix/unicode-slugs", "type": "user", "uuid": "9d8e2ffe-cd19-450b-a5c2-e62718c5d99d", "timestamp": "2026-09-18T14:03:58Z", "message": {"role": "user", "content": [{"tool_use_id": "toolu_867fd826f46a4126ac85342b", "type": "tool_result", "content": ".............. [100%]\n14 passed in 0.27s", "is_error": false}]}}
|
|
23
|
+
{"parentUuid": "9d8e2ffe-cd19-450b-a5c2-e62718c5d99d", "isSidechain": false, "userType": "external", "cwd": "/home/dev/slugkit", "sessionId": "5e55a0d0-de40-4c1e-9d3e-000000000001", "version": "2.1.270", "gitBranch": "fix/unicode-slugs", "type": "assistant", "uuid": "92e963d0-f683-4fd8-ae5a-4df0135eea6a", "timestamp": "2026-09-18T14:04:06Z", "message": {"model": "claude-opus-5", "role": "assistant", "type": "message", "content": [{"type": "tool_use", "id": "toolu_7fcddb74f9304f6b8f9ef07a", "name": "Bash", "input": {"command": "cd /home/dev/slugkit && git add -A && git commit -q -m \"slugify: fold accents, drop emoji, cap at 80 chars\" && git push -q", "description": "Commit and push"}}], "usage": {"input_tokens": 4, "cache_creation_input_tokens": 900, "cache_read_input_tokens": 33000, "output_tokens": 220}}}
|
|
24
|
+
{"parentUuid": "92e963d0-f683-4fd8-ae5a-4df0135eea6a", "isSidechain": false, "userType": "external", "cwd": "/home/dev/slugkit", "sessionId": "5e55a0d0-de40-4c1e-9d3e-000000000001", "version": "2.1.270", "gitBranch": "fix/unicode-slugs", "type": "user", "uuid": "b8dbc652-6557-48c1-ac3d-8a66f0bd24f1", "timestamp": "2026-09-18T14:04:08Z", "message": {"role": "user", "content": [{"tool_use_id": "toolu_7fcddb74f9304f6b8f9ef07a", "type": "tool_result", "content": "", "is_error": false}]}}
|
|
25
|
+
{"parentUuid": "b8dbc652-6557-48c1-ac3d-8a66f0bd24f1", "isSidechain": false, "userType": "external", "cwd": "/home/dev/slugkit", "sessionId": "5e55a0d0-de40-4c1e-9d3e-000000000001", "version": "2.1.270", "gitBranch": "fix/unicode-slugs", "type": "assistant", "uuid": "52dca5ee-d8de-4c15-a93f-646783fcd3e3", "timestamp": "2026-09-18T14:04:12Z", "message": {"model": "claude-opus-5", "role": "assistant", "type": "message", "content": [{"type": "text", "text": "Fixed. `slugify(\"Crème brûlée 🍮\")` now returns `creme-brulee`: titles are NFKD-normalised and folded to ASCII, symbols and emoji are dropped, and slugs are capped at 80 characters. Two new tests cover it; all 14 pass."}], "usage": {"input_tokens": 6, "cache_creation_input_tokens": 2100, "cache_read_input_tokens": 31000, "output_tokens": 180}}}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
http://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
-----------------------------------------------------------
|
|
8
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
9
|
+
-----------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
PREAMBLE
|
|
12
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
13
|
+
development of collaborative font projects, to support the font creation
|
|
14
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
15
|
+
open framework in which fonts may be shared and improved in partnership
|
|
16
|
+
with others.
|
|
17
|
+
|
|
18
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
19
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
20
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
21
|
+
redistributed and/or sold with any software provided that any reserved
|
|
22
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
23
|
+
however, cannot be released under any other type of license. The
|
|
24
|
+
requirement for fonts to remain under this license does not apply
|
|
25
|
+
to any document created using the fonts or their derivatives.
|
|
26
|
+
|
|
27
|
+
DEFINITIONS
|
|
28
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
29
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
30
|
+
include source files, build scripts and documentation.
|
|
31
|
+
|
|
32
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
33
|
+
copyright statement(s).
|
|
34
|
+
|
|
35
|
+
"Original Version" refers to the collection of Font Software components as
|
|
36
|
+
distributed by the Copyright Holder(s).
|
|
37
|
+
|
|
38
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
39
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
40
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
41
|
+
new environment.
|
|
42
|
+
|
|
43
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
44
|
+
writer or other person who contributed to the Font Software.
|
|
45
|
+
|
|
46
|
+
PERMISSION AND CONDITIONS
|
|
47
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
48
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
49
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
50
|
+
Software, subject to the following conditions:
|
|
51
|
+
|
|
52
|
+
1) Neither the Font Software nor any of its individual components,
|
|
53
|
+
in Original or Modified Versions, may be sold by itself.
|
|
54
|
+
|
|
55
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
56
|
+
redistributed and/or sold with any software, provided that each copy
|
|
57
|
+
contains the above copyright notice and this license. These can be
|
|
58
|
+
included either as stand-alone text files, human-readable headers or
|
|
59
|
+
in the appropriate machine-readable metadata fields within text or
|
|
60
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
61
|
+
|
|
62
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
63
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
64
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
65
|
+
presented to the users.
|
|
66
|
+
|
|
67
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
68
|
+
Software shall not be used to promote, endorse or advertise any
|
|
69
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
70
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
71
|
+
permission.
|
|
72
|
+
|
|
73
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
74
|
+
must be distributed entirely under this license, and must not be
|
|
75
|
+
distributed under any other license. The requirement for fonts to
|
|
76
|
+
remain under this license does not apply to any document created
|
|
77
|
+
using the Font Software.
|
|
78
|
+
|
|
79
|
+
TERMINATION
|
|
80
|
+
This license becomes null and void if any of the above conditions are
|
|
81
|
+
not met.
|
|
82
|
+
|
|
83
|
+
DISCLAIMER
|
|
84
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
85
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
86
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
87
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
88
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
89
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
90
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
91
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
92
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
https://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|