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.
- learnlance-0.1.0/LICENSE +21 -0
- learnlance-0.1.0/PKG-INFO +117 -0
- learnlance-0.1.0/README.md +89 -0
- learnlance-0.1.0/learnlance/__init__.py +6 -0
- learnlance-0.1.0/learnlance/__main__.py +6 -0
- learnlance-0.1.0/learnlance/cli.py +176 -0
- learnlance-0.1.0/learnlance/config.py +76 -0
- learnlance-0.1.0/learnlance/graph.py +208 -0
- learnlance-0.1.0/learnlance/hook.py +192 -0
- learnlance-0.1.0/learnlance/insights.py +175 -0
- learnlance-0.1.0/learnlance/install.py +79 -0
- learnlance-0.1.0/learnlance/transcript.py +106 -0
- learnlance-0.1.0/learnlance/viz.py +260 -0
- learnlance-0.1.0/learnlance.egg-info/PKG-INFO +117 -0
- learnlance-0.1.0/learnlance.egg-info/SOURCES.txt +18 -0
- learnlance-0.1.0/learnlance.egg-info/dependency_links.txt +1 -0
- learnlance-0.1.0/learnlance.egg-info/entry_points.txt +2 -0
- learnlance-0.1.0/learnlance.egg-info/top_level.txt +1 -0
- learnlance-0.1.0/pyproject.toml +41 -0
- learnlance-0.1.0/setup.cfg +4 -0
learnlance-0.1.0/LICENSE
ADDED
|
@@ -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,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
|