learnlance 0.1.0__tar.gz

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 aeroscissorz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.4
2
+ Name: learnlance
3
+ Version: 0.1.0
4
+ Summary: Turn what Claude Code builds into a growing personal knowledge graph.
5
+ Author: aeroscissorz
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/aeroscissorz/learnlance
8
+ Project-URL: Repository, https://github.com/aeroscissorz/learnlance
9
+ Project-URL: Issues, https://github.com/aeroscissorz/learnlance/issues
10
+ Keywords: claude,claude-code,learning,knowledge-graph,cli,hooks
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development
23
+ Classifier: Topic :: Education
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Dynamic: license-file
28
+
29
+ # learnlance 🧠🔍
30
+
31
+ A learning companion for **Claude Code**. Every time Claude finishes a turn and
32
+ has generated or edited code, learnlance quietly:
33
+
34
+ 1. reads the session transcript and pulls out the code that was just written,
35
+ 2. asks Claude *"what concepts could a developer learn from this?"* —
36
+ e.g. *"you used a **delta function**, here's what delta encoding is"*,
37
+ 3. merges those concepts into a **persistent knowledge graph**, and
38
+ 4. regenerates an interactive HTML graph you can open any time.
39
+
40
+ **No API key required.** By default it reuses the `claude` CLI you're already
41
+ logged into (your Claude Code subscription), so there's nothing extra to set up.
42
+
43
+ So instead of code just appearing, you build up a visual map of everything
44
+ you've picked up along the way — new nodes light up as *🌱 new topics learned*.
45
+
46
+ ## Why it won't slow you down or break your session
47
+ - The API call runs in a **detached background process** — Claude Code never waits.
48
+ - The hook is wrapped so any failure is logged and swallowed; it can never
49
+ interrupt your coding session.
50
+ - Turns with no substantive code make **no API call** (no cost, no noise).
51
+
52
+ ## Install (no pip, no API key)
53
+
54
+ ```bash
55
+ # just install the Claude Code Stop hook — it uses your logged-in `claude`
56
+ python -m learnlance install
57
+ ```
58
+
59
+ Run this from the `learnlance/` project folder. That's it — start (or restart)
60
+ Claude Code and code as usual.
61
+
62
+ If `claude` isn't on your PATH, point learnlance at it:
63
+
64
+ ```bash
65
+ python -m learnlance config --claude-bin "C:\path\to\claude.cmd"
66
+ ```
67
+
68
+ > Prefer a global `learnlance` command? `pip install -e .` in this folder, then
69
+ > use `learnlance` instead of `python -m learnlance` everywhere.
70
+ >
71
+ > Prefer a direct API call instead of the CLI? `learnlance config --backend api
72
+ > --set-key sk-ant-...`
73
+
74
+ ## Use it
75
+
76
+ ```bash
77
+ learnlance show # render + open the interactive knowledge graph in your browser
78
+ learnlance list -v # list learned concepts (with explanations) in the terminal
79
+ learnlance stats # quick counts, broken down by category
80
+ ```
81
+
82
+ Per-session markdown recaps are written to `~/.learnlance/insights/<session>.md`.
83
+
84
+ ## Configuration
85
+
86
+ ```bash
87
+ learnlance config # show current settings
88
+ learnlance config --cli-model haiku # make the cli backend use a faster model
89
+ learnlance config --max-topics 3 # fewer concepts per turn
90
+ learnlance config --background off # run inline (blocks until analysis is done)
91
+ learnlance config --disable # pause without uninstalling the hook
92
+ learnlance config --backend api --set-key sk-ant-... # switch to the API backend
93
+ ```
94
+
95
+ Everything lives under `~/.learnlance/`:
96
+ `graph.json` (the graph), `graph.html` (the visualization), `insights/`
97
+ (markdown recaps), `learnlance.log` (diagnostics).
98
+
99
+ ## Uninstall
100
+
101
+ ```bash
102
+ python -m learnlance uninstall
103
+ ```
104
+
105
+ ## How it works (internals)
106
+
107
+ | File | Role |
108
+ |------|------|
109
+ | `hook.py` | Stop-hook entry; spawns the detached worker |
110
+ | `transcript.py` | Parses Claude Code's JSONL transcript for generated code |
111
+ | `insights.py` | Generates insights — via the `claude` CLI (default) or the API |
112
+ | `graph.py` | Merges concepts into the persistent knowledge graph |
113
+ | `viz.py` | Renders the offline, self-contained HTML graph |
114
+ | `install.py` | Wires the hook into `~/.claude/settings.json` |
115
+
116
+ Zero third-party dependencies by design — the hook must run reliably wherever
117
+ Claude Code launches it.
@@ -0,0 +1,89 @@
1
+ # learnlance 🧠🔍
2
+
3
+ A learning companion for **Claude Code**. Every time Claude finishes a turn and
4
+ has generated or edited code, learnlance quietly:
5
+
6
+ 1. reads the session transcript and pulls out the code that was just written,
7
+ 2. asks Claude *"what concepts could a developer learn from this?"* —
8
+ e.g. *"you used a **delta function**, here's what delta encoding is"*,
9
+ 3. merges those concepts into a **persistent knowledge graph**, and
10
+ 4. regenerates an interactive HTML graph you can open any time.
11
+
12
+ **No API key required.** By default it reuses the `claude` CLI you're already
13
+ logged into (your Claude Code subscription), so there's nothing extra to set up.
14
+
15
+ So instead of code just appearing, you build up a visual map of everything
16
+ you've picked up along the way — new nodes light up as *🌱 new topics learned*.
17
+
18
+ ## Why it won't slow you down or break your session
19
+ - The API call runs in a **detached background process** — Claude Code never waits.
20
+ - The hook is wrapped so any failure is logged and swallowed; it can never
21
+ interrupt your coding session.
22
+ - Turns with no substantive code make **no API call** (no cost, no noise).
23
+
24
+ ## Install (no pip, no API key)
25
+
26
+ ```bash
27
+ # just install the Claude Code Stop hook — it uses your logged-in `claude`
28
+ python -m learnlance install
29
+ ```
30
+
31
+ Run this from the `learnlance/` project folder. That's it — start (or restart)
32
+ Claude Code and code as usual.
33
+
34
+ If `claude` isn't on your PATH, point learnlance at it:
35
+
36
+ ```bash
37
+ python -m learnlance config --claude-bin "C:\path\to\claude.cmd"
38
+ ```
39
+
40
+ > Prefer a global `learnlance` command? `pip install -e .` in this folder, then
41
+ > use `learnlance` instead of `python -m learnlance` everywhere.
42
+ >
43
+ > Prefer a direct API call instead of the CLI? `learnlance config --backend api
44
+ > --set-key sk-ant-...`
45
+
46
+ ## Use it
47
+
48
+ ```bash
49
+ learnlance show # render + open the interactive knowledge graph in your browser
50
+ learnlance list -v # list learned concepts (with explanations) in the terminal
51
+ learnlance stats # quick counts, broken down by category
52
+ ```
53
+
54
+ Per-session markdown recaps are written to `~/.learnlance/insights/<session>.md`.
55
+
56
+ ## Configuration
57
+
58
+ ```bash
59
+ learnlance config # show current settings
60
+ learnlance config --cli-model haiku # make the cli backend use a faster model
61
+ learnlance config --max-topics 3 # fewer concepts per turn
62
+ learnlance config --background off # run inline (blocks until analysis is done)
63
+ learnlance config --disable # pause without uninstalling the hook
64
+ learnlance config --backend api --set-key sk-ant-... # switch to the API backend
65
+ ```
66
+
67
+ Everything lives under `~/.learnlance/`:
68
+ `graph.json` (the graph), `graph.html` (the visualization), `insights/`
69
+ (markdown recaps), `learnlance.log` (diagnostics).
70
+
71
+ ## Uninstall
72
+
73
+ ```bash
74
+ python -m learnlance uninstall
75
+ ```
76
+
77
+ ## How it works (internals)
78
+
79
+ | File | Role |
80
+ |------|------|
81
+ | `hook.py` | Stop-hook entry; spawns the detached worker |
82
+ | `transcript.py` | Parses Claude Code's JSONL transcript for generated code |
83
+ | `insights.py` | Generates insights — via the `claude` CLI (default) or the API |
84
+ | `graph.py` | Merges concepts into the persistent knowledge graph |
85
+ | `viz.py` | Renders the offline, self-contained HTML graph |
86
+ | `install.py` | Wires the hook into `~/.claude/settings.json` |
87
+
88
+ Zero third-party dependencies by design — the hook must run reliably wherever
89
+ Claude Code launches it.
@@ -0,0 +1,6 @@
1
+ """learnlance — a learning companion for Claude Code.
2
+
3
+ After every Claude Code turn, it extracts the concepts you could learn from the
4
+ code that was generated and grows a personal knowledge graph.
5
+ """
6
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(main())
@@ -0,0 +1,176 @@
1
+ """learnlance command-line interface."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import sys
6
+ import webbrowser
7
+
8
+ from . import config, graph, hook, insights, install, viz
9
+
10
+
11
+ def _cmd_install(args):
12
+ print(install.install_hook())
13
+ cfg = config.load_config()
14
+ backend = cfg.get("backend", "cli")
15
+ if backend == "cli":
16
+ if insights.resolve_claude_bin(cfg):
17
+ print("\nBackend: cli — uses your logged-in `claude` (no API key needed).")
18
+ else:
19
+ print("\n⚠ Backend is 'cli' but `claude` wasn't found on PATH.")
20
+ print(" learnlance config --claude-bin \"C:\\path\\to\\claude.cmd\"")
21
+ else:
22
+ if not config.get_api_key(cfg):
23
+ print("\n⚠ Backend is 'api' but no API key is set:")
24
+ print(" learnlance config --set-key sk-ant-... (or export ANTHROPIC_API_KEY)")
25
+ print("\nDone. New Claude Code sessions will now build your knowledge graph.")
26
+
27
+
28
+ def _cmd_uninstall(args):
29
+ print(install.uninstall_hook())
30
+
31
+
32
+ def _cmd_config(args):
33
+ cfg = config.load_config()
34
+ changed = False
35
+ if args.backend is not None:
36
+ cfg["backend"] = args.backend
37
+ changed = True
38
+ if args.claude_bin is not None:
39
+ cfg["claude_bin"] = args.claude_bin
40
+ changed = True
41
+ if args.cli_model is not None:
42
+ cfg["cli_model"] = args.cli_model
43
+ changed = True
44
+ if args.set_key is not None:
45
+ cfg["api_key"] = args.set_key
46
+ changed = True
47
+ if args.model is not None:
48
+ cfg["model"] = args.model
49
+ changed = True
50
+ if args.enable:
51
+ cfg["enabled"] = True
52
+ changed = True
53
+ if args.disable:
54
+ cfg["enabled"] = False
55
+ changed = True
56
+ if args.background is not None:
57
+ cfg["background"] = args.background == "on"
58
+ changed = True
59
+ if args.max_topics is not None:
60
+ cfg["max_topics_per_turn"] = args.max_topics
61
+ changed = True
62
+ if changed:
63
+ config.save_config(cfg)
64
+ print("Saved config.")
65
+ # Show current state (mask the key)
66
+ shown = dict(cfg)
67
+ if shown.get("api_key"):
68
+ shown["api_key"] = shown["api_key"][:7] + "…"
69
+ key_src = "config" if cfg.get("api_key") else ("env" if config.get_api_key(cfg) else "MISSING")
70
+ print(f"\nConfig ({config.CONFIG_PATH}):")
71
+ for k, v in shown.items():
72
+ print(f" {k}: {v}")
73
+ print(f" api key source: {key_src}")
74
+
75
+
76
+ def _cmd_show(args):
77
+ g = graph.load()
78
+ path = viz.render_html(g)
79
+ print(f"Graph written to {path}")
80
+ if not args.no_open:
81
+ webbrowser.open(path.as_uri())
82
+
83
+
84
+ def _cmd_list(args):
85
+ g = graph.load()
86
+ nodes = [n for n in g.get("nodes", {}).values() if not n.get("placeholder")]
87
+ if not nodes:
88
+ print("Nothing learned yet. Install the hook and let Claude Code write some code.")
89
+ return
90
+ nodes.sort(key=lambda n: (-n.get("count", 0), n["name"].lower()))
91
+ print(f"{len(nodes)} concepts learned:\n")
92
+ for n in nodes:
93
+ print(f" • {n['name']} [{n.get('category','')}·{n.get('level','')}] seen {n.get('count',0)}×")
94
+ if args.verbose and n.get("explanation"):
95
+ print(f" {n['explanation']}")
96
+
97
+
98
+ def _cmd_stats(args):
99
+ g = graph.load()
100
+ nodes = g.get("nodes", {})
101
+ real = [n for n in nodes.values() if not n.get("placeholder")]
102
+ cats: dict[str, int] = {}
103
+ for n in real:
104
+ cats[n.get("category", "other")] = cats.get(n.get("category", "other"), 0) + 1
105
+ print(f"Concepts learned : {len(real)}")
106
+ print(f"Related links : {len(g.get('edges', []))}")
107
+ print(f"Turns analyzed : {g.get('meta', {}).get('turns', 0)}")
108
+ print(f"Sessions : {len(g.get('sessions', {}))}")
109
+ if cats:
110
+ print("\nBy category:")
111
+ for c, n in sorted(cats.items(), key=lambda x: -x[1]):
112
+ print(f" {c:18} {n}")
113
+
114
+
115
+ def _cmd_hook(args):
116
+ hook.run_hook()
117
+
118
+
119
+ def _cmd_worker(args):
120
+ hook.run_worker(args.job)
121
+
122
+
123
+ def build_parser() -> argparse.ArgumentParser:
124
+ p = argparse.ArgumentParser(prog="learnlance",
125
+ description="Turn what Claude Code builds into a growing knowledge graph.")
126
+ sub = p.add_subparsers(dest="cmd")
127
+
128
+ sub.add_parser("install", help="install the Claude Code Stop hook").set_defaults(func=_cmd_install)
129
+ sub.add_parser("uninstall", help="remove the Stop hook").set_defaults(func=_cmd_uninstall)
130
+
131
+ c = sub.add_parser("config", help="view/set configuration")
132
+ c.add_argument("--backend", choices=["cli", "api"],
133
+ help="cli = use logged-in `claude` (no key); api = Anthropic API")
134
+ c.add_argument("--claude-bin", dest="claude_bin", metavar="PATH",
135
+ help="path to the claude executable (cli backend)")
136
+ c.add_argument("--cli-model", dest="cli_model", metavar="MODEL",
137
+ help="optional model alias for the cli backend, e.g. haiku")
138
+ c.add_argument("--set-key", dest="set_key", metavar="KEY", help="Anthropic API key (api backend)")
139
+ c.add_argument("--model", help="model id for the api backend")
140
+ c.add_argument("--enable", action="store_true")
141
+ c.add_argument("--disable", action="store_true")
142
+ c.add_argument("--background", choices=["on", "off"], help="run API work detached")
143
+ c.add_argument("--max-topics", dest="max_topics", type=int)
144
+ c.set_defaults(func=_cmd_config)
145
+
146
+ s = sub.add_parser("show", help="render + open the HTML knowledge graph")
147
+ s.add_argument("--no-open", action="store_true", help="just write the file")
148
+ s.set_defaults(func=_cmd_show)
149
+
150
+ l = sub.add_parser("list", help="list learned concepts in the terminal")
151
+ l.add_argument("-v", "--verbose", action="store_true")
152
+ l.set_defaults(func=_cmd_list)
153
+
154
+ sub.add_parser("stats", help="summary counts").set_defaults(func=_cmd_stats)
155
+
156
+ h = sub.add_parser("hook", help="(internal) Stop-hook entry point")
157
+ h.set_defaults(func=_cmd_hook)
158
+ w = sub.add_parser("_worker", help=argparse.SUPPRESS)
159
+ w.add_argument("job")
160
+ w.set_defaults(func=_cmd_worker)
161
+
162
+ return p
163
+
164
+
165
+ def main(argv=None) -> int:
166
+ parser = build_parser()
167
+ args = parser.parse_args(argv)
168
+ if not getattr(args, "func", None):
169
+ parser.print_help()
170
+ return 0
171
+ args.func(args)
172
+ return 0
173
+
174
+
175
+ if __name__ == "__main__":
176
+ sys.exit(main())
@@ -0,0 +1,76 @@
1
+ """Configuration + on-disk paths for learnlance.
2
+
3
+ Everything lives under ~/.learnlance (override with LEARNLANCE_HOME).
4
+ No third-party dependencies anywhere in this package on purpose: the hook must
5
+ run reliably in whatever environment Claude Code launches it in.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ from pathlib import Path
12
+
13
+ HOME = Path(os.environ.get("LEARNLANCE_HOME", str(Path.home() / ".learnlance")))
14
+ CONFIG_PATH = HOME / "config.json"
15
+ GRAPH_PATH = HOME / "graph.json"
16
+ HTML_PATH = HOME / "graph.html"
17
+ STATE_PATH = HOME / "state.json"
18
+ INSIGHTS_DIR = HOME / "insights"
19
+ WORK_DIR = HOME / "work"
20
+ LOG_PATH = HOME / "learnlance.log"
21
+
22
+ DEFAULTS = {
23
+ "enabled": True,
24
+ # How insights are generated:
25
+ # "cli" -> shell out to the `claude` CLI you're already logged into
26
+ # (NO API key needed — uses your Claude Code subscription auth).
27
+ # "api" -> direct Anthropic API call (needs an API key).
28
+ "backend": "cli",
29
+ "claude_bin": "", # path to the `claude` executable ("" => auto-detect on PATH)
30
+ "cli_model": "", # optional model alias for the CLI (e.g. "haiku"); "" => default
31
+ # Used only by the "api" backend:
32
+ "model": "claude-haiku-4-5-20251001",
33
+ "api_key": "", # empty => fall back to ANTHROPIC_API_KEY env var
34
+ "max_topics_per_turn": 5,
35
+ "background": True, # run the work detached so Claude Code stays snappy
36
+ "min_chars": 40, # skip trivial edits (renames, one-liners) to save calls
37
+ "max_input_chars": 14000, # cap the code we send per turn
38
+ }
39
+
40
+ # Env flag set on any `claude` process we spawn, so the Stop hook it fires can
41
+ # recognize it's a learnlance-triggered call and bail instead of recursing.
42
+ REENTRY_FLAG = "LEARNLANCE_ACTIVE"
43
+
44
+
45
+ def ensure_home() -> None:
46
+ HOME.mkdir(parents=True, exist_ok=True)
47
+ INSIGHTS_DIR.mkdir(parents=True, exist_ok=True)
48
+ WORK_DIR.mkdir(parents=True, exist_ok=True)
49
+
50
+
51
+ def load_config() -> dict:
52
+ cfg = dict(DEFAULTS)
53
+ if CONFIG_PATH.exists():
54
+ try:
55
+ cfg.update(json.loads(CONFIG_PATH.read_text(encoding="utf-8")))
56
+ except Exception:
57
+ pass
58
+ return cfg
59
+
60
+
61
+ def save_config(cfg: dict) -> None:
62
+ ensure_home()
63
+ CONFIG_PATH.write_text(json.dumps(cfg, indent=2), encoding="utf-8")
64
+
65
+
66
+ def get_api_key(cfg: dict) -> str:
67
+ return (cfg.get("api_key") or "").strip() or os.environ.get("ANTHROPIC_API_KEY", "").strip()
68
+
69
+
70
+ def log(msg: str) -> None:
71
+ try:
72
+ ensure_home()
73
+ with LOG_PATH.open("a", encoding="utf-8") as fh:
74
+ fh.write(msg.rstrip() + "\n")
75
+ except Exception:
76
+ pass