footprint-trace 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/bin/banner.js +19 -0
- package/bin/footprint.js +24 -0
- package/footprint.py +216 -0
- package/package.json +17 -0
- package/skills/footprint/SKILL.md +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 footprint contributors
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# footprint
|
|
2
|
+
|
|
3
|
+
**Your Claude, learned locally.**
|
|
4
|
+
|
|
5
|
+
Footprint watches your Claude Code sessions, fine-tunes a small local model
|
|
6
|
+
(LoRA) on how *you and Claude* work in your projects, and serves it as an
|
|
7
|
+
OpenAI-compatible API. When your Claude quota runs out, OpenCode (or Cursor,
|
|
8
|
+
or Codex CLI) keeps working in the same style — fully offline, on your machine.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
trace → chat with claude → collect → train → install → /footprint in OpenCode
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## How it works
|
|
15
|
+
|
|
16
|
+
1. Claude Code already logs every session (prompts, replies, tool calls) to `~/.claude/projects`.
|
|
17
|
+
2. `footprint trace` arms a marker so only sessions from that point on become training data.
|
|
18
|
+
3. `footprint collect` parses those transcripts into chat-format training examples.
|
|
19
|
+
4. `footprint train` LoRA fine-tunes a small model (default: Qwen2.5-Coder-1.5B, 4-bit MLX) on them.
|
|
20
|
+
5. `footprint install` registers a launchd agent — the server runs itself at
|
|
21
|
+
`http://127.0.0.1:8399/v1`, starts at login, restarts on crash. You never run it by hand.
|
|
22
|
+
6. Any OpenAI-compatible tool points at that URL and behaves like your Claude.
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
| | trace / collect | train / serve |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| **macOS (Apple Silicon)** | ✅ | ✅ (MLX) |
|
|
29
|
+
| **macOS (Intel)** | ✅ | ❌ |
|
|
30
|
+
| **Linux** | ✅ | ❌ (for now) |
|
|
31
|
+
| **Windows** | ✅ | ❌ (for now) |
|
|
32
|
+
|
|
33
|
+
- [Claude Code](https://claude.com/claude-code) installed and used at least once
|
|
34
|
+
- Python 3.9+
|
|
35
|
+
- Node.js 18+ (only if installing via npm)
|
|
36
|
+
|
|
37
|
+
Training and serving use [MLX](https://github.com/ml-explore/mlx), which is
|
|
38
|
+
Apple-Silicon-only today. On Linux/Windows you can still trace and collect —
|
|
39
|
+
your `data/` folder is portable; train it on any Apple Silicon Mac. Other
|
|
40
|
+
backends (llama.cpp / unsloth) are welcome contributions.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
### macOS
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
npm install -g footprint-trace
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
or from source:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
git clone https://github.com/Amanlabh/footprint.git
|
|
54
|
+
cd footprint
|
|
55
|
+
python3 footprint.py setup
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Linux
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
npm install -g footprint-trace # or the git clone above
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`setup` skips MLX automatically. `trace` and `collect` work; copy `data/` to a Mac to train.
|
|
65
|
+
|
|
66
|
+
### Windows
|
|
67
|
+
|
|
68
|
+
Use PowerShell (Python 3 from python.org or the Microsoft Store):
|
|
69
|
+
|
|
70
|
+
```powershell
|
|
71
|
+
npm install -g footprint-trace
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Same scope as Linux: trace + collect. WSL2 works the same way.
|
|
75
|
+
|
|
76
|
+
## Getting started
|
|
77
|
+
|
|
78
|
+
**1. Arm tracing — before you start Claude.**
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
footprint trace
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Claude Code already logs every session; the trace marker makes footprint train
|
|
85
|
+
only on sessions from this point on. (Skip this step — or delete
|
|
86
|
+
`~/.claude/footprint-trace` — to train on your entire history instead.)
|
|
87
|
+
|
|
88
|
+
**2. Use Claude Code normally.**
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
claude
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Every session (your prompts, Claude's replies, tool calls) is captured automatically.
|
|
95
|
+
|
|
96
|
+
**3. Collect and train.**
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
footprint collect # transcripts -> data/train.jsonl + data/valid.jsonl
|
|
100
|
+
footprint train # LoRA fine-tune (~10 min, downloads ~1 GB base model first time)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**4. Install the always-on server (macOS).**
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
footprint install
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
This registers a launchd agent — the server starts at login, restarts if it
|
|
110
|
+
crashes, and you never run it by hand. It also wires up OpenCode:
|
|
111
|
+
|
|
112
|
+
- provider `footprint` at `http://127.0.0.1:8399/v1`
|
|
113
|
+
- a `/footprint <task>` command inside OpenCode
|
|
114
|
+
|
|
115
|
+
**5. Claude quota over? Keep going.**
|
|
116
|
+
|
|
117
|
+
Open OpenCode and type:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
/footprint fix the failing test in auth.py
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Or point any OpenAI-compatible tool at the server:
|
|
124
|
+
|
|
125
|
+
| Tool | Setting |
|
|
126
|
+
|---|---|
|
|
127
|
+
| **Cursor** | Settings → Models → OpenAI base URL `http://127.0.0.1:8399/v1`, any API key, add model `footprint` |
|
|
128
|
+
| **Codex CLI** | custom provider with that base URL in `~/.codex/config.toml` |
|
|
129
|
+
| **anything else** | base URL `http://127.0.0.1:8399/v1`, API key: none |
|
|
130
|
+
|
|
131
|
+
## Commands
|
|
132
|
+
|
|
133
|
+
| Command | What it does |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `footprint` | banner + status |
|
|
136
|
+
| `footprint trace` | arm tracing (run before starting Claude) |
|
|
137
|
+
| `footprint collect [dir]` | parse transcripts of a project (default: current dir; unknown dir = all projects) |
|
|
138
|
+
| `footprint train` | LoRA fine-tune on collected data |
|
|
139
|
+
| `footprint install` | launchd auto-server + OpenCode integration (macOS) |
|
|
140
|
+
| `footprint serve` | run the server manually (fallback; `install` makes this unnecessary) |
|
|
141
|
+
| `footprint status` | model, example count, adapter, tracing, server state |
|
|
142
|
+
|
|
143
|
+
## Configuration
|
|
144
|
+
|
|
145
|
+
| Env var | Default | |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| `FOOTPRINT_MODEL` | `mlx-community/Qwen2.5-Coder-1.5B-Instruct-4bit` | any MLX chat model |
|
|
148
|
+
| `FOOTPRINT_ITERS` | `300` | training iterations |
|
|
149
|
+
| `FOOTPRINT_PORT` | `8399` | server port |
|
|
150
|
+
|
|
151
|
+
## Troubleshooting
|
|
152
|
+
|
|
153
|
+
- **`no transcripts found`** — either you haven't used Claude Code in this project, or the trace marker is newer than all sessions. Chat first, or `rm ~/.claude/footprint-trace`.
|
|
154
|
+
- **server not responding** — check `/tmp/footprint-serve.log`; `launchctl kickstart -k gui/$UID/com.footprint.serve` restarts it.
|
|
155
|
+
- **`mlx_lm` import fails** — re-run `footprint setup` (Apple Silicon only).
|
|
156
|
+
- **quality is rough** — more data beats more iterations: keep tracing, re-collect, re-train. 1.5B is small; try `FOOTPRINT_MODEL=mlx-community/Qwen2.5-Coder-7B-Instruct-4bit` if you have ≥16 GB RAM.
|
|
157
|
+
|
|
158
|
+
## Privacy
|
|
159
|
+
|
|
160
|
+
Everything stays on your machine. `data/` (your transcripts) and `adapters/`
|
|
161
|
+
(weights trained on them) are gitignored — never commit or publish them.
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
MIT — see [LICENSE](LICENSE).
|
package/bin/banner.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
console.log(`
|
|
3
|
+
\x1b[36m███████╗ ██████╗ ██████╗ ████████╗██████╗ ██████╗ ██╗███╗ ██╗████████╗
|
|
4
|
+
██╔════╝██╔═══██╗██╔═══██╗╚══██╔══╝██╔══██╗██╔══██╗██║████╗ ██║╚══██╔══╝
|
|
5
|
+
█████╗ ██║ ██║██║ ██║ ██║ ██████╔╝██████╔╝██║██╔██╗ ██║ ██║
|
|
6
|
+
██╔══╝ ██║ ██║██║ ██║ ██║ ██╔═══╝ ██╔══██╗██║██║╚██╗██║ ██║
|
|
7
|
+
██║ ╚██████╔╝╚██████╔╝ ██║ ██║ ██║ ██║██║██║ ╚████║ ██║
|
|
8
|
+
╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝ ╚═╝\x1b[0m
|
|
9
|
+
|
|
10
|
+
your Claude, learned locally.
|
|
11
|
+
|
|
12
|
+
1. footprint trace arm tracing — do this BEFORE starting claude
|
|
13
|
+
2. claude chat as usual; every session is traced automatically
|
|
14
|
+
3. footprint collect turn traced sessions into training data
|
|
15
|
+
4. footprint train fine-tune the local model
|
|
16
|
+
5. footprint install always-on server + /footprint command in OpenCode
|
|
17
|
+
|
|
18
|
+
Claude quota over? open OpenCode, type /footprint <task> — works like Claude.
|
|
19
|
+
`);
|
package/bin/footprint.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { spawnSync } = require("child_process");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
|
|
6
|
+
const root = path.join(__dirname, "..");
|
|
7
|
+
const script = path.join(root, "footprint.py");
|
|
8
|
+
const venvPy = path.join(root, ".venv", "bin", "python");
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
|
|
11
|
+
if (args.length === 0) {
|
|
12
|
+
require("./banner");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(venvPy)) {
|
|
16
|
+
console.log("first run — setting up (venv + mlx-lm, takes a minute)…");
|
|
17
|
+
const s = spawnSync("python3", [script, "setup"], { stdio: "inherit" });
|
|
18
|
+
if (s.status) process.exit(s.status);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const r = spawnSync(venvPy, [script, ...(args.length ? args : ["status"])], {
|
|
22
|
+
stdio: "inherit",
|
|
23
|
+
});
|
|
24
|
+
process.exit(r.status === null ? 1 : r.status);
|
package/footprint.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""footprint — learn a local SLM from your Claude Code transcripts.
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
setup create .venv, install mlx-lm, link /footprint skill
|
|
6
|
+
trace arm tracing: sessions from now on become training data (run before chatting)
|
|
7
|
+
collect [project] parse ~/.claude/projects transcripts -> data/{train,valid}.jsonl
|
|
8
|
+
train LoRA fine-tune base model on collected data
|
|
9
|
+
serve OpenAI-compatible API at localhost:8399 (launchd runs this; users don't)
|
|
10
|
+
install launchd agent (server always on) + OpenCode /footprint command
|
|
11
|
+
status what's collected/trained
|
|
12
|
+
"""
|
|
13
|
+
import json, os, re, subprocess, sys, glob, random, time, plistlib
|
|
14
|
+
|
|
15
|
+
ROOT = os.path.dirname(os.path.abspath(__file__))
|
|
16
|
+
DATA = os.path.join(ROOT, "data")
|
|
17
|
+
ADAPTERS = os.path.join(ROOT, "adapters")
|
|
18
|
+
PROJECTS = os.path.expanduser("~/.claude/projects")
|
|
19
|
+
TRACE_MARK = os.path.expanduser("~/.claude/footprint-trace")
|
|
20
|
+
LAUNCHD = os.path.expanduser("~/Library/LaunchAgents/com.footprint.serve.plist")
|
|
21
|
+
OPENCODE = os.path.expanduser("~/.config/opencode")
|
|
22
|
+
MODEL = os.environ.get("FOOTPRINT_MODEL", "mlx-community/Qwen2.5-Coder-1.5B-Instruct-4bit")
|
|
23
|
+
CTX_BUDGET = 6000 # chars of history per training example
|
|
24
|
+
RESULT_CAP = 1500 # chars kept per tool result
|
|
25
|
+
|
|
26
|
+
SYSTEM = ("You are footprint, a local coding agent trained on Claude Code sessions. "
|
|
27
|
+
"To act, emit tool calls as <tool name=\"NAME\">{json args}</tool>. "
|
|
28
|
+
"Say DONE when the task is complete.")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _text(content, cap=None):
|
|
32
|
+
"""Flatten a Claude message content (str or block list) to training text."""
|
|
33
|
+
if isinstance(content, str):
|
|
34
|
+
out = content
|
|
35
|
+
else:
|
|
36
|
+
parts = []
|
|
37
|
+
for b in content or []:
|
|
38
|
+
t = b.get("type")
|
|
39
|
+
if t == "text":
|
|
40
|
+
parts.append(b["text"])
|
|
41
|
+
elif t == "tool_use":
|
|
42
|
+
parts.append(f'<tool name="{b["name"]}">{json.dumps(b.get("input", {}))}</tool>')
|
|
43
|
+
elif t == "tool_result":
|
|
44
|
+
c = b.get("content")
|
|
45
|
+
c = "".join(x.get("text", "") for x in c) if isinstance(c, list) else str(c or "")
|
|
46
|
+
parts.append(f"<tool_result>{c[:RESULT_CAP]}</tool_result>")
|
|
47
|
+
out = "\n".join(p for p in parts if p.strip())
|
|
48
|
+
out = re.sub(r"<system-reminder>.*?</system-reminder>", "", out, flags=re.S)
|
|
49
|
+
out = re.sub(r"<local-command-caveat>.*?</local-command-caveat>", "", out, flags=re.S)
|
|
50
|
+
return (out[:cap] if cap else out).strip()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def parse_session(path):
|
|
54
|
+
"""One transcript file -> list of alternating {role, content} messages."""
|
|
55
|
+
msgs = []
|
|
56
|
+
for line in open(path, errors="replace"):
|
|
57
|
+
try:
|
|
58
|
+
rec = json.loads(line)
|
|
59
|
+
except json.JSONDecodeError:
|
|
60
|
+
continue
|
|
61
|
+
if rec.get("type") not in ("user", "assistant") or rec.get("isSidechain") or rec.get("isMeta"):
|
|
62
|
+
continue
|
|
63
|
+
m = rec.get("message") or {}
|
|
64
|
+
role = m.get("role")
|
|
65
|
+
txt = _text(m.get("content"))
|
|
66
|
+
if not role or not txt:
|
|
67
|
+
continue
|
|
68
|
+
if msgs and msgs[-1]["role"] == role:
|
|
69
|
+
msgs[-1]["content"] += "\n" + txt
|
|
70
|
+
else:
|
|
71
|
+
msgs.append({"role": role, "content": txt})
|
|
72
|
+
return msgs
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def trace():
|
|
76
|
+
"""Arm tracing. Claude Code already logs every session to ~/.claude/projects;
|
|
77
|
+
the marker makes collect use only sessions started after this moment."""
|
|
78
|
+
os.makedirs(os.path.dirname(TRACE_MARK), exist_ok=True)
|
|
79
|
+
with open(TRACE_MARK, "w") as f:
|
|
80
|
+
f.write(str(time.time()))
|
|
81
|
+
print("tracing armed. chat with Claude Code as usual — those sessions become training data.\n"
|
|
82
|
+
"then: footprint.py collect && footprint.py train\n"
|
|
83
|
+
"(delete ~/.claude/footprint-trace to collect ALL history again)")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def collect(project=None):
|
|
87
|
+
since = float(open(TRACE_MARK).read()) if os.path.exists(TRACE_MARK) else 0.0
|
|
88
|
+
slug = "-" + os.path.abspath(project or os.getcwd()).strip("/").replace("/", "-").replace(".", "-")
|
|
89
|
+
dirs = [os.path.join(PROJECTS, slug)] if os.path.isdir(os.path.join(PROJECTS, slug)) \
|
|
90
|
+
else glob.glob(os.path.join(PROJECTS, "*"))
|
|
91
|
+
examples = []
|
|
92
|
+
for d in dirs:
|
|
93
|
+
for f in glob.glob(os.path.join(d, "*.jsonl")):
|
|
94
|
+
if os.path.getmtime(f) < since:
|
|
95
|
+
continue
|
|
96
|
+
msgs = parse_session(f)
|
|
97
|
+
# one example per assistant turn, with preceding context under budget
|
|
98
|
+
for i, m in enumerate(msgs):
|
|
99
|
+
if m["role"] != "assistant":
|
|
100
|
+
continue
|
|
101
|
+
ctx, size = [], 0
|
|
102
|
+
for prev in reversed(msgs[:i]):
|
|
103
|
+
size += len(prev["content"])
|
|
104
|
+
if size > CTX_BUDGET:
|
|
105
|
+
break
|
|
106
|
+
ctx.insert(0, prev)
|
|
107
|
+
if not ctx or ctx[0]["role"] != "user":
|
|
108
|
+
continue
|
|
109
|
+
examples.append({"messages": [{"role": "system", "content": SYSTEM}, *ctx, m]})
|
|
110
|
+
if not examples:
|
|
111
|
+
hint = " (trace marker active — only sessions after `trace` count; delete ~/.claude/footprint-trace for all history)" if since else ""
|
|
112
|
+
sys.exit(f"no transcripts found under {PROJECTS}{hint}")
|
|
113
|
+
random.seed(0)
|
|
114
|
+
random.shuffle(examples)
|
|
115
|
+
n_valid = max(1, len(examples) // 10)
|
|
116
|
+
os.makedirs(DATA, exist_ok=True)
|
|
117
|
+
for name, chunk in [("valid", examples[:n_valid]), ("train", examples[n_valid:])]:
|
|
118
|
+
with open(os.path.join(DATA, f"{name}.jsonl"), "w") as fh:
|
|
119
|
+
for e in chunk:
|
|
120
|
+
fh.write(json.dumps(e) + "\n")
|
|
121
|
+
print(f"collected {len(examples)} examples ({len(examples)-n_valid} train / {n_valid} valid) -> {DATA}")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def train():
|
|
125
|
+
if not os.path.exists(os.path.join(DATA, "train.jsonl")):
|
|
126
|
+
sys.exit("no data. run: footprint.py collect")
|
|
127
|
+
subprocess.run([sys.executable, "-m", "mlx_lm", "lora", "--model", MODEL, "--train",
|
|
128
|
+
"--data", DATA, "--adapter-path", ADAPTERS,
|
|
129
|
+
"--iters", os.environ.get("FOOTPRINT_ITERS", "300"),
|
|
130
|
+
"--max-seq-length", "4096", "--batch-size", "1"], check=True)
|
|
131
|
+
print(f"adapter saved -> {ADAPTERS}")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def serve():
|
|
135
|
+
port = os.environ.get("FOOTPRINT_PORT", "8399")
|
|
136
|
+
args = [sys.executable, "-m", "mlx_lm", "server", "--model", MODEL,
|
|
137
|
+
"--host", "127.0.0.1", "--port", port]
|
|
138
|
+
if os.path.exists(os.path.join(ADAPTERS, "adapters.safetensors")):
|
|
139
|
+
args += ["--adapter-path", ADAPTERS]
|
|
140
|
+
else:
|
|
141
|
+
print("(no adapter yet — serving base model; run train first for footprint behavior)")
|
|
142
|
+
print(f"OpenAI-compatible API: http://127.0.0.1:{port}/v1 (model name: footprint)\n"
|
|
143
|
+
"point Cursor / OpenCode / Codex / any OpenAI-compatible client here, api key: none")
|
|
144
|
+
subprocess.run(args, check=True)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def install():
|
|
148
|
+
"""Server runs itself (launchd, KeepAlive) + OpenCode /footprint command. User never runs serve."""
|
|
149
|
+
if sys.platform != "darwin":
|
|
150
|
+
sys.exit("install is macOS-only (launchd + MLX). on Linux/Windows run serve manually once MLX alternatives land.")
|
|
151
|
+
py = os.path.join(ROOT, ".venv", "bin", "python")
|
|
152
|
+
if not os.path.exists(py):
|
|
153
|
+
sys.exit("no .venv. run: python3 footprint.py setup")
|
|
154
|
+
port = os.environ.get("FOOTPRINT_PORT", "8399")
|
|
155
|
+
os.makedirs(os.path.dirname(LAUNCHD), exist_ok=True)
|
|
156
|
+
with open(LAUNCHD, "wb") as f:
|
|
157
|
+
plistlib.dump({"Label": "com.footprint.serve",
|
|
158
|
+
"ProgramArguments": [py, os.path.join(ROOT, "footprint.py"), "serve"],
|
|
159
|
+
"RunAtLoad": True, "KeepAlive": True,
|
|
160
|
+
"StandardOutPath": "/tmp/footprint-serve.log",
|
|
161
|
+
"StandardErrorPath": "/tmp/footprint-serve.log"}, f)
|
|
162
|
+
subprocess.run(["launchctl", "unload", LAUNCHD], capture_output=True)
|
|
163
|
+
subprocess.run(["launchctl", "load", "-w", LAUNCHD], check=True)
|
|
164
|
+
|
|
165
|
+
cfg_path = os.path.join(OPENCODE, "opencode.json")
|
|
166
|
+
cfg = json.load(open(cfg_path)) if os.path.exists(cfg_path) else {"$schema": "https://opencode.ai/config.json"}
|
|
167
|
+
cfg.setdefault("provider", {})["footprint"] = {
|
|
168
|
+
"npm": "@ai-sdk/openai-compatible", "name": "footprint",
|
|
169
|
+
"options": {"baseURL": f"http://127.0.0.1:{port}/v1"},
|
|
170
|
+
"models": {"footprint": {"name": "footprint"}}}
|
|
171
|
+
os.makedirs(os.path.join(OPENCODE, "command"), exist_ok=True)
|
|
172
|
+
with open(cfg_path, "w") as f:
|
|
173
|
+
json.dump(cfg, f, indent=2)
|
|
174
|
+
with open(os.path.join(OPENCODE, "command", "footprint.md"), "w") as f:
|
|
175
|
+
f.write("---\ndescription: do task with footprint — local model trained on your Claude sessions\n"
|
|
176
|
+
"model: footprint/footprint\n---\n$ARGUMENTS\n")
|
|
177
|
+
print(f"installed:\n"
|
|
178
|
+
f" server: launchd keeps http://127.0.0.1:{port}/v1 running (log: /tmp/footprint-serve.log)\n"
|
|
179
|
+
f" opencode: /footprint <task> uses it; or pick model 'footprint' via /models\n"
|
|
180
|
+
f" cursor/codex: base URL http://127.0.0.1:{port}/v1, any api key")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def status():
|
|
184
|
+
tr = os.path.join(DATA, "train.jsonl")
|
|
185
|
+
n = sum(1 for _ in open(tr)) if os.path.exists(tr) else 0
|
|
186
|
+
print(f"model: {MODEL}\nexamples: {n}\nadapter: "
|
|
187
|
+
f"{'yes' if os.path.exists(os.path.join(ADAPTERS, 'adapters.safetensors')) else 'no'}\n"
|
|
188
|
+
f"tracing: {'armed since ' + time.ctime(float(open(TRACE_MARK).read())) if os.path.exists(TRACE_MARK) else 'off (collect uses all history)'}\n"
|
|
189
|
+
f"server: {'launchd-managed' if os.path.exists(LAUNCHD) else 'not installed (run install)'}")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def setup():
|
|
193
|
+
venv = os.path.join(ROOT, ".venv")
|
|
194
|
+
if not os.path.exists(venv):
|
|
195
|
+
subprocess.run([sys.executable, "-m", "venv", venv], check=True)
|
|
196
|
+
if sys.platform == "darwin":
|
|
197
|
+
subprocess.run([os.path.join(venv, "bin", "pip"), "install", "-q", "mlx-lm"], check=True)
|
|
198
|
+
else:
|
|
199
|
+
print("mlx-lm skipped (Apple Silicon only). trace/collect work here; "
|
|
200
|
+
"copy data/ to a Mac for train/serve.")
|
|
201
|
+
link = os.path.expanduser("~/.claude/skills/footprint")
|
|
202
|
+
os.makedirs(os.path.dirname(link), exist_ok=True)
|
|
203
|
+
if not os.path.exists(link):
|
|
204
|
+
os.symlink(os.path.join(ROOT, "skills", "footprint"), link)
|
|
205
|
+
print(f"ready. /footprint skill linked -> {link}\nuse: {venv}/bin/python footprint.py collect|train|run")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
if __name__ == "__main__":
|
|
209
|
+
cmd = sys.argv[1] if len(sys.argv) > 1 else "status"
|
|
210
|
+
{"collect": lambda: collect(sys.argv[2] if len(sys.argv) > 2 else None),
|
|
211
|
+
"trace": trace,
|
|
212
|
+
"train": train,
|
|
213
|
+
"serve": serve,
|
|
214
|
+
"install": install,
|
|
215
|
+
"status": status,
|
|
216
|
+
"setup": setup}.get(cmd, lambda: sys.exit(__doc__))()
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "footprint-trace",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local SLM trained on your Claude Code sessions. When Claude quota runs out, OpenCode keeps working like Claude.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"footprint": "bin/footprint.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"postinstall": "node bin/banner.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"footprint.py",
|
|
14
|
+
"skills"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT"
|
|
17
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: footprint
|
|
3
|
+
description: Local SLM that learns from Claude Code transcripts ("footprint") and replays the project's working style. Trigger on /footprint, "take footprint", "train footprint", "footprint status", "footprint run <task>".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Footprint
|
|
7
|
+
|
|
8
|
+
Footprint collects this machine's Claude Code session transcripts, fine-tunes a
|
|
9
|
+
small local model (MLX LoRA, Apple Silicon) on them, and can then execute tasks
|
|
10
|
+
locally with a tiny bash-tool agent loop — no API needed.
|
|
11
|
+
|
|
12
|
+
Repo: `/Users/aman/Developer/footprint`. Python: `/Users/aman/Developer/footprint/.venv/bin/python`
|
|
13
|
+
(if `.venv` missing, run `setup` first with system `python3`).
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
Map the user's `/footprint <arg>` to one Bash call:
|
|
18
|
+
|
|
19
|
+
| Invocation | Run |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `/footprint` or `/footprint status` | `.venv/bin/python footprint.py status` |
|
|
22
|
+
| `/footprint setup` | `python3 footprint.py setup` |
|
|
23
|
+
| `/footprint trace` | `.venv/bin/python footprint.py trace` (arm BEFORE chatting; only sessions after this become training data) |
|
|
24
|
+
| `/footprint collect` | `.venv/bin/python footprint.py collect <current project dir>` |
|
|
25
|
+
| `/footprint collect all` | `.venv/bin/python footprint.py collect --all-projects-nonexistent-dir` (falls back to all projects) |
|
|
26
|
+
| `/footprint train` | `.venv/bin/python footprint.py train` (long; run in background) |
|
|
27
|
+
| `/footprint install` | `.venv/bin/python footprint.py install` (launchd auto-server + OpenCode `/footprint` command — user never runs serve) |
|
|
28
|
+
| `/footprint serve` | `.venv/bin/python footprint.py serve` (manual fallback only; install makes this unnecessary) |
|
|
29
|
+
|
|
30
|
+
Always run commands from `/Users/aman/Developer/footprint`.
|
|
31
|
+
|
|
32
|
+
## Flow for a fresh user
|
|
33
|
+
|
|
34
|
+
1. `setup` — venv + mlx-lm + skill symlink.
|
|
35
|
+
2. `trace` — arm tracing FIRST, before chatting with Claude. Writes `~/.claude/footprint-trace`; collect then only uses sessions newer than the marker (delete marker to use all history).
|
|
36
|
+
3. Chat with Claude Code normally — sessions land in `~/.claude/projects` automatically.
|
|
37
|
+
4. `collect` — parses `~/.claude/projects/<project>/*.jsonl` into `data/train.jsonl` + `data/valid.jsonl` (chat format, tool calls encoded as `<tool name="...">{args}</tool>`).
|
|
38
|
+
5. `train` — LoRA on `mlx-community/Qwen2.5-Coder-1.5B-Instruct-4bit` (override with `FOOTPRINT_MODEL`, iterations with `FOOTPRINT_ITERS`, default 300). Downloads ~1 GB first time. Run in background, report loss when done.
|
|
39
|
+
6. `install` — the user never runs the server. launchd (`com.footprint.serve`, KeepAlive) keeps the OpenAI-compatible API at `http://127.0.0.1:8399/v1` running across reboots/crashes. Also installs into OpenCode: provider `footprint` + `/footprint <task>` command (`~/.config/opencode/command/footprint.md`) so typing `/footprint` there makes OpenCode work like Claude.
|
|
40
|
+
Other tools point at the always-on server:
|
|
41
|
+
- **Cursor**: Settings → Models → OpenAI base URL `http://127.0.0.1:8399/v1`, any API key, add model `footprint`.
|
|
42
|
+
- **Codex CLI**: set a custom provider with that base URL in `~/.codex/config.toml`.
|
|
43
|
+
|
|
44
|
+
Report command output tersely. If mlx-lm import fails, rerun `setup`.
|