cairn-mcp 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.
- cairn_mcp-0.1.0/.claude-plugin/plugin.json +13 -0
- cairn_mcp-0.1.0/.gitignore +28 -0
- cairn_mcp-0.1.0/.mcp.json +8 -0
- cairn_mcp-0.1.0/LICENSE +21 -0
- cairn_mcp-0.1.0/PKG-INFO +146 -0
- cairn_mcp-0.1.0/README.md +129 -0
- cairn_mcp-0.1.0/cairn_mcp/__init__.py +8 -0
- cairn_mcp-0.1.0/cairn_mcp/__main__.py +5 -0
- cairn_mcp-0.1.0/cairn_mcp/client.py +88 -0
- cairn_mcp-0.1.0/cairn_mcp/file_logger.py +50 -0
- cairn_mcp-0.1.0/cairn_mcp/server.py +379 -0
- cairn_mcp-0.1.0/cairn_mcp/session_logger.py +146 -0
- cairn_mcp-0.1.0/hooks/hooks.json +28 -0
- cairn_mcp-0.1.0/pyproject.toml +30 -0
- cairn_mcp-0.1.0/uv.lock +984 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cairn",
|
|
3
|
+
"displayName": "Cairn",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Log Claude Code work sessions, file edits, and tasks to your Cairn account. Bundles the Cairn MCP server (11 tools) and the session + file-edit auto-logging hooks.",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "cybort360",
|
|
8
|
+
"url": "https://github.com/cybort360"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/cybort360/cairn",
|
|
11
|
+
"repository": "https://github.com/cybort360/cairn",
|
|
12
|
+
"keywords": ["work-tracking", "productivity", "mcp", "logging", "tasks"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
.venv/
|
|
5
|
+
|
|
6
|
+
# Vercel
|
|
7
|
+
.vercel
|
|
8
|
+
.env*.local
|
|
9
|
+
|
|
10
|
+
# Superseded legacy code — kept on disk, not part of the product repo.
|
|
11
|
+
# webapp.py: pre-serverless SQLite monolith (replaced by lib/ + api/ + dev.py)
|
|
12
|
+
# server.py: old local-SQLite MCP (replaced by the cloud client in agent/)
|
|
13
|
+
/webapp.py
|
|
14
|
+
/server.py
|
|
15
|
+
|
|
16
|
+
# OS
|
|
17
|
+
.DS_Store
|
|
18
|
+
|
|
19
|
+
# Playwright test artifacts
|
|
20
|
+
.playwright-mcp/
|
|
21
|
+
*.png
|
|
22
|
+
|
|
23
|
+
# Subagent-driven dev scratch (ledger, briefs, diffs)
|
|
24
|
+
.superpowers/
|
|
25
|
+
|
|
26
|
+
# Python build artifacts
|
|
27
|
+
dist/
|
|
28
|
+
*.egg-info/
|
cairn_mcp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adedeji Olamide
|
|
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.
|
cairn_mcp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cairn-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server that logs Claude Code work sessions, file edits, and tasks to your Cairn account.
|
|
5
|
+
Project-URL: Homepage, https://github.com/cybort360/cairn
|
|
6
|
+
Project-URL: Repository, https://github.com/cybort360/cairn
|
|
7
|
+
Author: Adedeji Olamide
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: claude-code,mcp,model-context-protocol,productivity,work-tracking
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Requires-Dist: mcp[cli]>=1.0.0
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# The Cairn agent (MCP server + hooks)
|
|
19
|
+
|
|
20
|
+
This folder is the piece that runs on *your* machine, not on Vercel. It gives Claude Code a set of Cairn tools (log work, list tasks, weekly summaries, and so on) and, if you want, quietly records what you did after every session. Everything here is a thin HTTPS client over your Cairn cloud account — no local database, no second copy of your data.
|
|
21
|
+
|
|
22
|
+
What's in here:
|
|
23
|
+
|
|
24
|
+
- `cairn_mcp/` — the Python package, published to PyPI as [`cairn-mcp`](https://pypi.org/project/cairn-mcp/):
|
|
25
|
+
- `server.py` — the MCP server. Eleven tools (`work_log_add`, `task_create`, `daily_summary`, …).
|
|
26
|
+
- `client.py` — the tiny stdlib HTTP client the server and hooks share. Reads your token, talks to the API.
|
|
27
|
+
- `session_logger.py` — a SessionEnd hook. Turns a finished Claude Code session into one work-log entry.
|
|
28
|
+
- `file_logger.py` — a PostToolUse hook. Records a file-activity event whenever Claude writes or edits a file.
|
|
29
|
+
- `pyproject.toml` — packaging metadata. One dependency, `mcp[cli]`; one console script, `cairn-mcp`.
|
|
30
|
+
- `.mcp.json`, `hooks/hooks.json`, `.claude-plugin/` — the Claude Code plugin wiring.
|
|
31
|
+
|
|
32
|
+
## Easiest: install as a plugin
|
|
33
|
+
|
|
34
|
+
If you're on Claude Code, skip the manual steps below — this repo is also a plugin marketplace. Two commands from inside Claude Code:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
/plugin marketplace add cybort360/cairn
|
|
38
|
+
/plugin install cairn@cairn
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
That registers the `cairn` MCP server and both auto-logging hooks for you; there's no clone, no venv, and no `claude mcp add`. One prerequisite: the plugin runs the server through [`uv`](https://docs.astral.sh/uv/), so you need `uv` on your PATH (`curl -LsSf https://astral.sh/uv/install.sh | sh`). `uv` pulls `mcp[cli]` on its own, so you never manage a Python env.
|
|
42
|
+
|
|
43
|
+
You still need to drop your token in once — see step 2 of the manual install for where to get it:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
printf '%s' 'PASTE_YOUR_TOKEN' > ~/.cairn_token && chmod 600 ~/.cairn_token
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## From PyPI (any MCP client)
|
|
50
|
+
|
|
51
|
+
Not on Claude Code, or want the server without the plugin? The package is on PyPI, so [`uv`](https://docs.astral.sh/uv/) can run it with nothing to clone or install:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
claude mcp add cairn --scope user -- uvx cairn-mcp
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`uvx cairn-mcp` is the command any MCP client can point at; `uvx` fetches the package and its deps on first run. Save your token first (see step 2 below). For a different client, use its own config — the command is the same.
|
|
58
|
+
|
|
59
|
+
## Manual, from source
|
|
60
|
+
|
|
61
|
+
For when you'd rather not use `uv` at all. You need Python 3.10+ and the Claude Code CLI.
|
|
62
|
+
|
|
63
|
+
**1. Clone and install the package into a venv.**
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
git clone https://github.com/cybort360/cairn.git
|
|
67
|
+
cd cairn/agent
|
|
68
|
+
python3 -m venv .venv
|
|
69
|
+
.venv/bin/pip install .
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
That puts a `cairn-mcp` console script in `.venv/bin/`.
|
|
73
|
+
|
|
74
|
+
**2. Get your API token.** Sign in at the Cairn web app, open the account menu, and copy your API token. Save it where the client looks for it:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
printf '%s' 'PASTE_YOUR_TOKEN_HERE' > ~/.cairn_token
|
|
78
|
+
chmod 600 ~/.cairn_token
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
(You can set `CAIRN_TOKEN` in the environment instead if you'd rather not write a file. The client checks `CAIRN_TOKEN` first, then `~/.cairn_token`.)
|
|
82
|
+
|
|
83
|
+
**3. Register the server with Claude Code.** Point it at the console script. `--scope user` makes the `cairn` tools available in every project, not just this one:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
claude mcp add cairn --scope user -- "$(pwd)/.venv/bin/cairn-mcp"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**4. Check it connected.**
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
claude mcp list
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
You want to see `cairn: … ✔ Connected`. Inside a session the tools show up as `mcp__cairn__work_log_add`, `mcp__cairn__task_list`, and the rest.
|
|
96
|
+
|
|
97
|
+
That's the whole install. The hooks below are optional.
|
|
98
|
+
|
|
99
|
+
## Optional: auto-logging hooks
|
|
100
|
+
|
|
101
|
+
The plugin install already wires these up. Set them by hand only if you went the manual route. The two hooks make Cairn fill itself in without you calling a tool: `session_logger` writes one entry summarizing each session when it ends; `file_logger` notes every file Claude touches. Both fail silently — a dropped network call or a missing token never interrupts your work. They use only the standard library, so plain `python3` runs them; no `mcp` needed.
|
|
102
|
+
|
|
103
|
+
Wire them into Claude Code's settings (`~/.claude/settings.json` for all projects). The commands run the package modules, so they need `cairn_mcp` on the path — `cd` into wherever you cloned (`$HOME/cairn/agent` assumes `~/cairn`; adjust it):
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"hooks": {
|
|
108
|
+
"SessionEnd": [
|
|
109
|
+
{
|
|
110
|
+
"hooks": [
|
|
111
|
+
{
|
|
112
|
+
"type": "command",
|
|
113
|
+
"command": "cd \"$HOME/cairn/agent\" && python3 -m cairn_mcp.session_logger 2>/dev/null || true",
|
|
114
|
+
"timeout": 15,
|
|
115
|
+
"statusMessage": "Logging session to Cairn"
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
],
|
|
120
|
+
"PostToolUse": [
|
|
121
|
+
{
|
|
122
|
+
"matcher": "Write|Edit|MultiEdit",
|
|
123
|
+
"hooks": [
|
|
124
|
+
{
|
|
125
|
+
"type": "command",
|
|
126
|
+
"command": "cd \"$HOME/cairn/agent\" && python3 -m cairn_mcp.file_logger 2>/dev/null || true",
|
|
127
|
+
"timeout": 10
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
A hard kill (crash, power loss, `kill -9`) skips the SessionEnd hook, so that one session won't be logged. Everything else — the normal exit, `/clear`, `/logout`, Ctrl-D — fires it.
|
|
137
|
+
|
|
138
|
+
## Pointing at a different backend
|
|
139
|
+
|
|
140
|
+
By default the client talks to production. Override it with `CAIRN_API_URL` if you're running the web app locally or against a staging deploy:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
export CAIRN_API_URL="http://127.0.0.1:8765"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The MCP server reads it the same way, so set it in the `claude mcp add` environment (`-e CAIRN_API_URL=…`) if you want the tools pointed somewhere other than prod.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# The Cairn agent (MCP server + hooks)
|
|
2
|
+
|
|
3
|
+
This folder is the piece that runs on *your* machine, not on Vercel. It gives Claude Code a set of Cairn tools (log work, list tasks, weekly summaries, and so on) and, if you want, quietly records what you did after every session. Everything here is a thin HTTPS client over your Cairn cloud account — no local database, no second copy of your data.
|
|
4
|
+
|
|
5
|
+
What's in here:
|
|
6
|
+
|
|
7
|
+
- `cairn_mcp/` — the Python package, published to PyPI as [`cairn-mcp`](https://pypi.org/project/cairn-mcp/):
|
|
8
|
+
- `server.py` — the MCP server. Eleven tools (`work_log_add`, `task_create`, `daily_summary`, …).
|
|
9
|
+
- `client.py` — the tiny stdlib HTTP client the server and hooks share. Reads your token, talks to the API.
|
|
10
|
+
- `session_logger.py` — a SessionEnd hook. Turns a finished Claude Code session into one work-log entry.
|
|
11
|
+
- `file_logger.py` — a PostToolUse hook. Records a file-activity event whenever Claude writes or edits a file.
|
|
12
|
+
- `pyproject.toml` — packaging metadata. One dependency, `mcp[cli]`; one console script, `cairn-mcp`.
|
|
13
|
+
- `.mcp.json`, `hooks/hooks.json`, `.claude-plugin/` — the Claude Code plugin wiring.
|
|
14
|
+
|
|
15
|
+
## Easiest: install as a plugin
|
|
16
|
+
|
|
17
|
+
If you're on Claude Code, skip the manual steps below — this repo is also a plugin marketplace. Two commands from inside Claude Code:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
/plugin marketplace add cybort360/cairn
|
|
21
|
+
/plugin install cairn@cairn
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
That registers the `cairn` MCP server and both auto-logging hooks for you; there's no clone, no venv, and no `claude mcp add`. One prerequisite: the plugin runs the server through [`uv`](https://docs.astral.sh/uv/), so you need `uv` on your PATH (`curl -LsSf https://astral.sh/uv/install.sh | sh`). `uv` pulls `mcp[cli]` on its own, so you never manage a Python env.
|
|
25
|
+
|
|
26
|
+
You still need to drop your token in once — see step 2 of the manual install for where to get it:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
printf '%s' 'PASTE_YOUR_TOKEN' > ~/.cairn_token && chmod 600 ~/.cairn_token
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## From PyPI (any MCP client)
|
|
33
|
+
|
|
34
|
+
Not on Claude Code, or want the server without the plugin? The package is on PyPI, so [`uv`](https://docs.astral.sh/uv/) can run it with nothing to clone or install:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
claude mcp add cairn --scope user -- uvx cairn-mcp
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`uvx cairn-mcp` is the command any MCP client can point at; `uvx` fetches the package and its deps on first run. Save your token first (see step 2 below). For a different client, use its own config — the command is the same.
|
|
41
|
+
|
|
42
|
+
## Manual, from source
|
|
43
|
+
|
|
44
|
+
For when you'd rather not use `uv` at all. You need Python 3.10+ and the Claude Code CLI.
|
|
45
|
+
|
|
46
|
+
**1. Clone and install the package into a venv.**
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
git clone https://github.com/cybort360/cairn.git
|
|
50
|
+
cd cairn/agent
|
|
51
|
+
python3 -m venv .venv
|
|
52
|
+
.venv/bin/pip install .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
That puts a `cairn-mcp` console script in `.venv/bin/`.
|
|
56
|
+
|
|
57
|
+
**2. Get your API token.** Sign in at the Cairn web app, open the account menu, and copy your API token. Save it where the client looks for it:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
printf '%s' 'PASTE_YOUR_TOKEN_HERE' > ~/.cairn_token
|
|
61
|
+
chmod 600 ~/.cairn_token
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
(You can set `CAIRN_TOKEN` in the environment instead if you'd rather not write a file. The client checks `CAIRN_TOKEN` first, then `~/.cairn_token`.)
|
|
65
|
+
|
|
66
|
+
**3. Register the server with Claude Code.** Point it at the console script. `--scope user` makes the `cairn` tools available in every project, not just this one:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
claude mcp add cairn --scope user -- "$(pwd)/.venv/bin/cairn-mcp"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**4. Check it connected.**
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
claude mcp list
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
You want to see `cairn: … ✔ Connected`. Inside a session the tools show up as `mcp__cairn__work_log_add`, `mcp__cairn__task_list`, and the rest.
|
|
79
|
+
|
|
80
|
+
That's the whole install. The hooks below are optional.
|
|
81
|
+
|
|
82
|
+
## Optional: auto-logging hooks
|
|
83
|
+
|
|
84
|
+
The plugin install already wires these up. Set them by hand only if you went the manual route. The two hooks make Cairn fill itself in without you calling a tool: `session_logger` writes one entry summarizing each session when it ends; `file_logger` notes every file Claude touches. Both fail silently — a dropped network call or a missing token never interrupts your work. They use only the standard library, so plain `python3` runs them; no `mcp` needed.
|
|
85
|
+
|
|
86
|
+
Wire them into Claude Code's settings (`~/.claude/settings.json` for all projects). The commands run the package modules, so they need `cairn_mcp` on the path — `cd` into wherever you cloned (`$HOME/cairn/agent` assumes `~/cairn`; adjust it):
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"hooks": {
|
|
91
|
+
"SessionEnd": [
|
|
92
|
+
{
|
|
93
|
+
"hooks": [
|
|
94
|
+
{
|
|
95
|
+
"type": "command",
|
|
96
|
+
"command": "cd \"$HOME/cairn/agent\" && python3 -m cairn_mcp.session_logger 2>/dev/null || true",
|
|
97
|
+
"timeout": 15,
|
|
98
|
+
"statusMessage": "Logging session to Cairn"
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
"PostToolUse": [
|
|
104
|
+
{
|
|
105
|
+
"matcher": "Write|Edit|MultiEdit",
|
|
106
|
+
"hooks": [
|
|
107
|
+
{
|
|
108
|
+
"type": "command",
|
|
109
|
+
"command": "cd \"$HOME/cairn/agent\" && python3 -m cairn_mcp.file_logger 2>/dev/null || true",
|
|
110
|
+
"timeout": 10
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
A hard kill (crash, power loss, `kill -9`) skips the SessionEnd hook, so that one session won't be logged. Everything else — the normal exit, `/clear`, `/logout`, Ctrl-D — fires it.
|
|
120
|
+
|
|
121
|
+
## Pointing at a different backend
|
|
122
|
+
|
|
123
|
+
By default the client talks to production. Override it with `CAIRN_API_URL` if you're running the web app locally or against a staging deploy:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
export CAIRN_API_URL="http://127.0.0.1:8765"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The MCP server reads it the same way, so set it in the `claude mcp add` environment (`-e CAIRN_API_URL=…`) if you want the tools pointed somewhere other than prod.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Cairn MCP server and Claude Code hooks — a thin client over the Cairn cloud API.
|
|
2
|
+
|
|
3
|
+
Kept import-light on purpose: the hooks (session_logger, file_logger) run under a
|
|
4
|
+
bare ``python3`` with only the standard library, so importing this package must not
|
|
5
|
+
pull in ``mcp`` or ``pydantic``. The server lives in :mod:`cairn_mcp.server`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Tiny stdlib HTTP client for the Cairn cloud API.
|
|
2
|
+
|
|
3
|
+
The MCP server and the Claude Code hooks use this to talk to the user's Cairn
|
|
4
|
+
account over HTTPS, authenticating with their API token. No third-party deps.
|
|
5
|
+
|
|
6
|
+
Config:
|
|
7
|
+
CAIRN_API_URL base URL (defaults to production)
|
|
8
|
+
CAIRN_TOKEN API token, else read from ~/.cairn_token
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import urllib.error
|
|
14
|
+
import urllib.request
|
|
15
|
+
from urllib.parse import urlencode
|
|
16
|
+
|
|
17
|
+
DEFAULT_BASE = "https://cairn-octanes-projects.vercel.app"
|
|
18
|
+
TOKEN_PATH = os.path.expanduser("~/.cairn_token")
|
|
19
|
+
# Older installs saved the token here; read it as a fallback during the rename.
|
|
20
|
+
LEGACY_TOKEN_PATH = os.path.expanduser("~/.work_tracker_token")
|
|
21
|
+
DEFAULT_TIMEOUT = 5.0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CairnError(Exception):
|
|
25
|
+
"""Raised on missing config, network failure, or a non-2xx response."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def base_url() -> str:
|
|
29
|
+
return os.environ.get("CAIRN_API_URL", DEFAULT_BASE).rstrip("/")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def token() -> str:
|
|
33
|
+
t = os.environ.get("CAIRN_TOKEN")
|
|
34
|
+
if t:
|
|
35
|
+
return t.strip()
|
|
36
|
+
for path in (TOKEN_PATH, LEGACY_TOKEN_PATH):
|
|
37
|
+
try:
|
|
38
|
+
with open(path) as f:
|
|
39
|
+
return f.read().strip()
|
|
40
|
+
except OSError:
|
|
41
|
+
continue
|
|
42
|
+
return ""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _request(method, path, params=None, payload=None, timeout=DEFAULT_TIMEOUT):
|
|
46
|
+
tok = token()
|
|
47
|
+
if not tok:
|
|
48
|
+
raise CairnError("not configured — save your token to ~/.cairn_token "
|
|
49
|
+
"or set CAIRN_TOKEN")
|
|
50
|
+
url = base_url() + path
|
|
51
|
+
if params:
|
|
52
|
+
q = urlencode({k: v for k, v in params.items() if v is not None})
|
|
53
|
+
if q:
|
|
54
|
+
url += "?" + q
|
|
55
|
+
body = json.dumps(payload).encode() if payload is not None else None
|
|
56
|
+
req = urllib.request.Request(url, data=body, method=method)
|
|
57
|
+
req.add_header("Authorization", f"Bearer {tok}")
|
|
58
|
+
if body is not None:
|
|
59
|
+
req.add_header("Content-Type", "application/json")
|
|
60
|
+
try:
|
|
61
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
62
|
+
text = resp.read().decode()
|
|
63
|
+
return json.loads(text) if text else {}
|
|
64
|
+
except urllib.error.HTTPError as e:
|
|
65
|
+
detail = ""
|
|
66
|
+
try:
|
|
67
|
+
detail = json.loads(e.read().decode()).get("error", "")
|
|
68
|
+
except Exception:
|
|
69
|
+
pass
|
|
70
|
+
raise CairnError(f"HTTP {e.code}: {detail or e.reason}")
|
|
71
|
+
except urllib.error.URLError as e:
|
|
72
|
+
raise CairnError(f"network error: {e.reason}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get(path, params=None, timeout=DEFAULT_TIMEOUT):
|
|
76
|
+
return _request("GET", path, params=params, timeout=timeout)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def post(path, payload, timeout=DEFAULT_TIMEOUT):
|
|
80
|
+
return _request("POST", path, payload=payload, timeout=timeout)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def patch(path, payload, timeout=DEFAULT_TIMEOUT):
|
|
84
|
+
return _request("PATCH", path, payload=payload, timeout=timeout)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def delete(path, timeout=DEFAULT_TIMEOUT):
|
|
88
|
+
return _request("DELETE", path, timeout=timeout)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PostToolUse(Edit|Write|MultiEdit) hook for Cairn.
|
|
4
|
+
|
|
5
|
+
After Claude Code writes or edits a file, this records one file-activity event
|
|
6
|
+
in the user's Cairn account over HTTPS. Dedup (collapsing rapid repeats of the
|
|
7
|
+
same file+action) and language detection now happen server-side.
|
|
8
|
+
|
|
9
|
+
Fail-soft: any network/config error is swallowed so editing never breaks.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from . import client as cairn_client
|
|
17
|
+
|
|
18
|
+
POST_TIMEOUT = 3.0
|
|
19
|
+
|
|
20
|
+
# Paths we never want cluttering the activity log.
|
|
21
|
+
IGNORE_SUBSTRINGS = ("/.git/", "/node_modules/", "/.venv/", "/__pycache__/",
|
|
22
|
+
"/dist/", "/build/", "/.next/", ".work_tracker.db")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main() -> None:
|
|
26
|
+
try:
|
|
27
|
+
payload = json.load(sys.stdin)
|
|
28
|
+
except (json.JSONDecodeError, ValueError):
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
tool = payload.get("tool_name", "")
|
|
32
|
+
fp = (payload.get("tool_input") or {}).get("file_path", "")
|
|
33
|
+
if not fp or any(s in fp for s in IGNORE_SUBSTRINGS):
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
action = "created" if tool == "Write" else "edited"
|
|
37
|
+
cwd = payload.get("cwd") or os.getcwd()
|
|
38
|
+
project = os.path.basename(cwd.rstrip("/")) or None
|
|
39
|
+
display = os.path.relpath(fp, cwd) if fp.startswith(cwd) else fp
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
cairn_client.post("/api/files", {
|
|
43
|
+
"file_path": display, "action": action, "project": project,
|
|
44
|
+
}, timeout=POST_TIMEOUT)
|
|
45
|
+
except cairn_client.CairnError:
|
|
46
|
+
pass # fail-soft
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
main()
|