throughline 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 +329 -0
- package/bin/throughline.mjs +78 -0
- package/package.json +33 -0
- package/src/cli/doctor.mjs +98 -0
- package/src/cli/install.mjs +109 -0
- package/src/cli/status.mjs +41 -0
- package/src/constants.mjs +16 -0
- package/src/db.mjs +201 -0
- package/src/haiku-summarizer.mjs +100 -0
- package/src/resume-context.mjs +148 -0
- package/src/sc-detail.mjs +212 -0
- package/src/session-merger.mjs +127 -0
- package/src/session-merger.test.mjs +151 -0
- package/src/session-start.mjs +67 -0
- package/src/state-file.mjs +117 -0
- package/src/token-estimator.mjs +16 -0
- package/src/token-monitor.mjs +237 -0
- package/src/transcript-reader.mjs +364 -0
- package/src/transcript-reader.test.mjs +292 -0
- package/src/transcript-usage.mjs +128 -0
- package/src/turn-processor.mjs +272 -0
- package/src/turn-processor.test.mjs +155 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kitepon
|
|
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,329 @@
|
|
|
1
|
+
# Throughline
|
|
2
|
+
|
|
3
|
+
**Cut ~90% of Claude Code's context usage while keeping nearly all the memory.**
|
|
4
|
+
|
|
5
|
+
In a typical Claude Code session, **80% of the context window is tool I/O** —
|
|
6
|
+
file reads, Bash output, grep results. This data is consumed the moment Claude
|
|
7
|
+
acts on it, but it stays in the context forever, pushing you toward the window
|
|
8
|
+
limit.
|
|
9
|
+
|
|
10
|
+
Throughline fixes this by separating conversation content by **type, not time**:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
Without Throughline (50 turns, no /clear):
|
|
14
|
+
Context = user text + assistant text + tool I/O + system messages
|
|
15
|
+
≈ 125,000 tokens (80% is tool I/O you'll never re-read)
|
|
16
|
+
|
|
17
|
+
With Throughline (50 turns → /clear → resume):
|
|
18
|
+
Context = recent 20 turns of conversation text (L2)
|
|
19
|
+
+ older 30 turns as one-line summaries (L1)
|
|
20
|
+
+ zero tool I/O (L3 — retired to SQLite, on-demand)
|
|
21
|
+
≈ 13,000 tokens — same decisions, same context, 90% lighter
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Unlike MemGPT or LangChain's SummaryBufferMemory which compress by **recency**
|
|
25
|
+
(old = summarized), Throughline separates by **content type**: human-readable
|
|
26
|
+
conversation stays, machine-generated tool output retires. This is purpose-built
|
|
27
|
+
for coding assistants where tool I/O is heavy but transient.
|
|
28
|
+
|
|
29
|
+
The retired L3 data isn't lost — Claude can pull it back on demand via
|
|
30
|
+
`throughline detail <time>` when a past turn's tool output becomes relevant
|
|
31
|
+
again.
|
|
32
|
+
|
|
33
|
+
Throughline also ships a multi-session **token monitor** that reads real
|
|
34
|
+
Anthropic API usage from the transcript JSONL (no `length / 4` heuristics).
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install -g throughline
|
|
42
|
+
throughline install
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
That's it. `install` registers Throughline's hooks in `~/.claude/settings.json`
|
|
46
|
+
(user scope), so every Claude Code project on your machine picks it up
|
|
47
|
+
automatically. No per-project wiring required.
|
|
48
|
+
|
|
49
|
+
Start any Claude Code session and your turns will begin flowing into
|
|
50
|
+
`~/.throughline/throughline.db` in the background.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Three-layer memory model (schema v4)
|
|
55
|
+
|
|
56
|
+
| Layer | Name | Where it lives | Content | Cost per turn |
|
|
57
|
+
| ----- | ---------- | --------------------- | ---------------------------------------------------------- | ------------- |
|
|
58
|
+
| **L1** | Skeleton | injected when old | one-line Haiku-generated summary of the turn | ~10 tok |
|
|
59
|
+
| **L2** | Body | injected when recent | user text + assistant reply, verbatim | full natural |
|
|
60
|
+
| **L3** | Detail | SQLite only | tool I/O, system messages, images (on-demand via command) | heavy, retired |
|
|
61
|
+
|
|
62
|
+
The layers are **complementary and disjoint** — nothing is duplicated across
|
|
63
|
+
them. Thinking blocks are discarded entirely (not stored at either layer) to
|
|
64
|
+
match the stock Claude Code behavior.
|
|
65
|
+
|
|
66
|
+
On `SessionStart`, Throughline rebuilds the context from SQLite and
|
|
67
|
+
injects it as plain text:
|
|
68
|
+
|
|
69
|
+
- The **most recent 20 turns** are injected as full L2 (`bodies`) text
|
|
70
|
+
- **Older turns** are injected as L1 (`skeletons`) one-liners
|
|
71
|
+
- L3 stays in SQLite and is retrieved on demand via `/sc-detail <time>`
|
|
72
|
+
|
|
73
|
+
L1 summaries are generated by **Claude Haiku 4.5** via a subprocess
|
|
74
|
+
(`claude -p --model claude-haiku-4-5-*`), reusing your Claude Max login — no API
|
|
75
|
+
key required. Summarization is lazy: for sessions that stay under 20 turns,
|
|
76
|
+
Haiku is never invoked, so short tasks cost zero summarization time.
|
|
77
|
+
|
|
78
|
+
All three layers (L1/L2/L3) have working write paths as of schema v5.
|
|
79
|
+
`/sc-detail HH:MM:SS` returns user/assistant text (L2) plus a kind-grouped view
|
|
80
|
+
of tool inputs, tool outputs, and hook output captured at L3 for that turn.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## `/clear`-safe with memory rebonding
|
|
85
|
+
|
|
86
|
+
When you run `/clear`, the conversation transcript is discarded, but the SQLite
|
|
87
|
+
database is untouched. On the next session start:
|
|
88
|
+
|
|
89
|
+
1. `SessionStart` hook fires with a new `session_id`
|
|
90
|
+
2. Throughline finds the previous session in the same project
|
|
91
|
+
3. It **rebonds** all `skeletons` / `bodies` / `details` rows from the previous
|
|
92
|
+
session into the new session (via `UPDATE session_id = ?`) inside a
|
|
93
|
+
`BEGIN IMMEDIATE` transaction
|
|
94
|
+
4. A handover banner is injected:
|
|
95
|
+
`## Throughline: セッション記憶(N ターン引き継ぎ)`
|
|
96
|
+
|
|
97
|
+
Each row keeps its **origin_session_id**, so memories accumulate through chains
|
|
98
|
+
of `/clear` rather than being lost or overwritten:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
S1 (4 turns) -- /clear --> S2 (merges S1, adds 3 turns) -- /clear --> S3 (merges S2, adds 5 turns)
|
|
102
|
+
origin=S1×4 origin=S1×4, S2×3, S3×5
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
No time-window heuristic, no PID guessing, no ancestor walking. Just a
|
|
106
|
+
deterministic UPDATE inside a SQLite transaction.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Multi-session token monitor
|
|
111
|
+
|
|
112
|
+
Run:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
throughline monitor # all active sessions in the current project
|
|
116
|
+
throughline monitor --all # every project, every session
|
|
117
|
+
throughline monitor --session <id-prefix>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Example output (real values from a running 1M-context Opus session):
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
[Throughline] 1 セッション
|
|
124
|
+
▶ Throughline 2ed5039c ████░░░░░░░░░░░░░░░░ 205.1k / 21% 残 794.9k claude-opus-4-6
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
- **Token counts are accurate.** Read straight from the latest `message.usage`
|
|
128
|
+
field in the session transcript JSONL, which is what Anthropic's API actually
|
|
129
|
+
reported (`input_tokens + cache_creation_input_tokens + cache_read_input_tokens`).
|
|
130
|
+
No `length / 4` approximation.
|
|
131
|
+
- **1M-context detection** is automatic. It checks the `[1m]` suffix in the
|
|
132
|
+
transcript, falls back to string matching on `1M context`, and finally
|
|
133
|
+
promotes to 1M if observed usage exceeds 200k.
|
|
134
|
+
- **Multi-session view.** Each Claude Code session writes its own state file
|
|
135
|
+
(`~/.throughline/state/<session_id>.json`). The monitor scans the directory
|
|
136
|
+
every second and displays one row per live session, sorted by last activity.
|
|
137
|
+
The most recent one is highlighted with `▶`.
|
|
138
|
+
- **Stale hiding.** Sessions that haven't been touched in 15 minutes drop out of
|
|
139
|
+
the default view; files older than 24 hours are deleted entirely. This is the
|
|
140
|
+
only time threshold in the system and is used solely for display hygiene — no
|
|
141
|
+
memory decisions are made from it.
|
|
142
|
+
- **Line-wrap safe.** Each line is truncated to `process.stdout.columns - 1`
|
|
143
|
+
before drawing, preserving ANSI color codes. The redraw cursor math cannot
|
|
144
|
+
desync on narrow terminals.
|
|
145
|
+
|
|
146
|
+
### VS Code auto-start
|
|
147
|
+
|
|
148
|
+
For contributors working on Throughline itself, a `.vscode/tasks.json` in this
|
|
149
|
+
repo launches `throughline monitor` automatically in a dedicated terminal when
|
|
150
|
+
you open the folder. Drop an equivalent config into your own project's
|
|
151
|
+
`.vscode/tasks.json` to get the same behavior.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Commands
|
|
156
|
+
|
|
157
|
+
| Command | What it does |
|
|
158
|
+
| ---------------------------------------------- | ------------------------------------------------------------ |
|
|
159
|
+
| `throughline install` | Register hooks in `~/.claude/settings.json` (user scope) |
|
|
160
|
+
| `throughline install --project` | Register hooks in `.claude/settings.json` for this repo only |
|
|
161
|
+
| `throughline uninstall` | Remove Throughline hooks from the settings file |
|
|
162
|
+
| `throughline monitor [--all] [--session <id>]` | Run the multi-session token monitor |
|
|
163
|
+
| `throughline detail <time>` | Retrieve L2 body text and L3 tool I/O for a turn (see below) |
|
|
164
|
+
| `throughline doctor` | Check Node version, hook registration, DB writability, PATH |
|
|
165
|
+
| `throughline status` | Print DB statistics (sessions, skeletons, bodies, details) |
|
|
166
|
+
| `throughline --version` | Print the installed version |
|
|
167
|
+
|
|
168
|
+
Hook subcommands (invoked by Claude Code, not by humans):
|
|
169
|
+
`session-start` (SessionStart), `process-turn` (Stop).
|
|
170
|
+
|
|
171
|
+
### `throughline detail` — for AI, not humans
|
|
172
|
+
|
|
173
|
+
`throughline detail` is the escape hatch Claude itself uses to pull archived
|
|
174
|
+
detail back into the context when an L1 summary isn't enough. The injection
|
|
175
|
+
footer explicitly instructs Claude to run this via its Bash tool when a past
|
|
176
|
+
turn's tool I/O becomes relevant.
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
throughline detail 14:23:05 # single timestamp
|
|
180
|
+
throughline detail 14:23-14:30 # timestamp range
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Output groups records by `kind`: L2 conversation bodies, then L3 tool input/
|
|
184
|
+
output, then system messages (hook output), then images. Records are scoped to
|
|
185
|
+
the current project's merge chain so Claude only sees turns from its own
|
|
186
|
+
project history.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Requirements
|
|
191
|
+
|
|
192
|
+
- **Node.js >= 22.5** (for the built-in `node:sqlite` module — no native build
|
|
193
|
+
required, no `npm install` of SQLite bindings)
|
|
194
|
+
- **Claude Code** with hooks support (`SessionStart`, `Stop`)
|
|
195
|
+
- **Claude Max subscription** (for Haiku-based L1 summarization via `claude -p`)
|
|
196
|
+
- Works on **Windows, macOS, Linux**
|
|
197
|
+
|
|
198
|
+
Throughline has **zero runtime dependencies**. The published tarball is just
|
|
199
|
+
plain `.mjs` files.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Data layout
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
~/.throughline/
|
|
207
|
+
├── throughline.db SQLite database (WAL mode)
|
|
208
|
+
├── haiku-workdir/ Isolated cwd for Haiku subprocess (recursion guard)
|
|
209
|
+
└── state/
|
|
210
|
+
└── <session_id>.json Per-session activity state for the monitor
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Schema v5:
|
|
214
|
+
|
|
215
|
+
- `sessions` — one row per `session_id`, with `project_path` and `merged_into`
|
|
216
|
+
- `skeletons` — L1 one-liners, keyed by `(session_id, origin_session_id, turn, role)`
|
|
217
|
+
- `bodies` — L2 verbatim text (user + assistant), same key shape
|
|
218
|
+
- `details` — L3 records with `kind` column (`tool_input` / `tool_output` / `system` / `image`) and `source_id` for idempotent re-processing
|
|
219
|
+
- `injection_log` — audit trail of injection events
|
|
220
|
+
|
|
221
|
+
All tables carry an `origin_session_id` so rebonded rows keep their lineage
|
|
222
|
+
after a `/clear` chain.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Design principle: no fallback code
|
|
227
|
+
|
|
228
|
+
Throughline deliberately refuses to swallow unexpected errors.
|
|
229
|
+
Silent `try { … } catch { /* ignore */ }` blocks hide bugs; instead, hooks throw
|
|
230
|
+
and exit with a non-zero status so Claude Code surfaces the failure in `stderr`.
|
|
231
|
+
|
|
232
|
+
Specifically:
|
|
233
|
+
|
|
234
|
+
- JSON parse failures → `throw`, not `continue`
|
|
235
|
+
- Missing required fields → `throw new Error(...)`, not `exit(0)`
|
|
236
|
+
- DB transactions → explicit `BEGIN IMMEDIATE` / `ROLLBACK` / re-throw
|
|
237
|
+
- Hook entry points wrap `main()` with a single `.catch` that writes `stderr` and
|
|
238
|
+
exits with code 1
|
|
239
|
+
|
|
240
|
+
The only tolerated silent paths are:
|
|
241
|
+
- JSONL per-line parse tolerance (tail partial writes are part of the format spec)
|
|
242
|
+
- State-file corruption recovery (files are idempotently regenerated next turn)
|
|
243
|
+
|
|
244
|
+
See [`docs/PUBLIC_RELEASE_PLAN.md §0`](docs/PUBLIC_RELEASE_PLAN.md) for the full
|
|
245
|
+
rule.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Haiku recursion defense
|
|
250
|
+
|
|
251
|
+
L1 summarization spawns `claude -p --model claude-haiku-4-5-*` as a subprocess.
|
|
252
|
+
Without precautions this would recursively fire the same Stop hook on the
|
|
253
|
+
subprocess and infinite-loop. Two defenses stack:
|
|
254
|
+
|
|
255
|
+
1. **Isolated cwd.** The subprocess runs in `~/.throughline/haiku-workdir/`, a
|
|
256
|
+
directory that contains no `.claude/settings.json`, so project-local hooks
|
|
257
|
+
are never picked up by the child.
|
|
258
|
+
2. **Env var guard.** The parent sets
|
|
259
|
+
`THROUGHLINE_IN_HAIKU_SUBPROCESS=1` in the child env. The Stop hook
|
|
260
|
+
(`turn-processor.mjs`) exits immediately on line 1 if it sees this variable.
|
|
261
|
+
|
|
262
|
+
See [`src/haiku-summarizer.mjs`](src/haiku-summarizer.mjs) for the implementation.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Troubleshooting
|
|
267
|
+
|
|
268
|
+
**Monitor says `待機中 — アクティブなセッションがありません`**
|
|
269
|
+
No session has touched its state file in the last 15 minutes. Send a message in
|
|
270
|
+
Claude Code and the monitor should pick it up within 1 second. If it still does
|
|
271
|
+
not, run `throughline doctor`.
|
|
272
|
+
|
|
273
|
+
**`throughline install` wrote to the wrong settings file**
|
|
274
|
+
By default, Throughline installs to `~/.claude/settings.json` (user scope, applies
|
|
275
|
+
to all projects). Use `--project` to scope it to the current directory's
|
|
276
|
+
`.claude/settings.json` instead.
|
|
277
|
+
|
|
278
|
+
**Hooks never fire**
|
|
279
|
+
Run `throughline doctor` — it checks Node version, hook registration, DB
|
|
280
|
+
writability, and PATH resolution. If the binary is not on PATH, reinstall with
|
|
281
|
+
`npm install -g throughline`.
|
|
282
|
+
|
|
283
|
+
**`node:sqlite` warning on startup**
|
|
284
|
+
Node.js prints `ExperimentalWarning: SQLite is an experimental feature` on stderr.
|
|
285
|
+
This is cosmetic — the module is stable enough for production and is used
|
|
286
|
+
unchanged here.
|
|
287
|
+
|
|
288
|
+
**Database got corrupted / want a clean slate**
|
|
289
|
+
Delete `~/.throughline/throughline.db` (and the `-shm` / `-wal` companion files)
|
|
290
|
+
and `~/.throughline/state/*.json`. A fresh database with schema v4 is created on
|
|
291
|
+
the next hook fire.
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## Development
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
git clone https://github.com/kitepon-rgb/Throughline.git
|
|
299
|
+
cd Throughline
|
|
300
|
+
npm link # Put `throughline` on PATH
|
|
301
|
+
throughline install --project # Register hooks for this repo only
|
|
302
|
+
node --test src/turn-processor.test.mjs src/session-merger.test.mjs
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Run the monitor directly without a global install:
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
node src/token-monitor.mjs
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
The `.vscode/tasks.json` in this repo auto-launches the monitor when you open
|
|
312
|
+
the folder in VS Code.
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Design docs
|
|
317
|
+
|
|
318
|
+
- [`docs/L1_L2_L3_REDESIGN.md`](docs/L1_L2_L3_REDESIGN.md) — **current design
|
|
319
|
+
spec** for the L1/L2/L3 differential layer model (schema v4). Authoritative.
|
|
320
|
+
- [`docs/PUBLIC_RELEASE_PLAN.md`](docs/PUBLIC_RELEASE_PLAN.md) — public release
|
|
321
|
+
plan (CLI surface, package.json layout, § 0 fallback rule)
|
|
322
|
+
- [`docs/archive/`](docs/archive/) — superseded design documents kept for
|
|
323
|
+
historical reference (original CONCEPT, session-linking experiments, etc.)
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## License
|
|
328
|
+
|
|
329
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* throughline CLI ディスパッチャ
|
|
4
|
+
* サブコマンドに応じて既存の hook スクリプトへ委譲する。
|
|
5
|
+
*
|
|
6
|
+
* 使い方:
|
|
7
|
+
* throughline install # ~/.claude/settings.json に hook を登録
|
|
8
|
+
* throughline uninstall # hook を削除
|
|
9
|
+
* throughline process-turn # Stop hook (Claude Code から呼ばれる)
|
|
10
|
+
* throughline session-start # SessionStart hook (Claude Code から呼ばれる)
|
|
11
|
+
* throughline detail <時刻> # L2+L3 詳細取得 (Claude が Bash 経由で呼ぶ想定)
|
|
12
|
+
* throughline doctor # 環境チェック
|
|
13
|
+
* throughline status # DB 統計表示
|
|
14
|
+
* throughline --version # バージョン表示
|
|
15
|
+
*
|
|
16
|
+
* 注意: schema v4 で capture-tool (PostToolUse) は廃止。L2/L3 は Stop 内で一括処理。
|
|
17
|
+
* inject-context (UserPromptSubmit) も廃止。L1/L2 注入は SessionStart で 1 回のみ。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const [, , cmd, ...rest] = process.argv;
|
|
21
|
+
|
|
22
|
+
switch (cmd) {
|
|
23
|
+
case 'install':
|
|
24
|
+
await (await import('../src/cli/install.mjs')).run(rest);
|
|
25
|
+
break;
|
|
26
|
+
case 'uninstall':
|
|
27
|
+
await (await import('../src/cli/install.mjs')).run(['--uninstall', ...rest]);
|
|
28
|
+
break;
|
|
29
|
+
case 'process-turn':
|
|
30
|
+
await import('../src/turn-processor.mjs');
|
|
31
|
+
break;
|
|
32
|
+
case 'session-start':
|
|
33
|
+
await import('../src/session-start.mjs');
|
|
34
|
+
break;
|
|
35
|
+
case 'monitor':
|
|
36
|
+
await import('../src/token-monitor.mjs');
|
|
37
|
+
break;
|
|
38
|
+
case 'detail':
|
|
39
|
+
(await import('../src/sc-detail.mjs')).run(rest);
|
|
40
|
+
break;
|
|
41
|
+
case 'doctor':
|
|
42
|
+
await (await import('../src/cli/doctor.mjs')).run();
|
|
43
|
+
break;
|
|
44
|
+
case 'status':
|
|
45
|
+
await (await import('../src/cli/status.mjs')).run();
|
|
46
|
+
break;
|
|
47
|
+
case '--version':
|
|
48
|
+
case '-v': {
|
|
49
|
+
const { createRequire } = await import('node:module');
|
|
50
|
+
const require = createRequire(import.meta.url);
|
|
51
|
+
const pkg = require('../package.json');
|
|
52
|
+
console.log(pkg.version);
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
default:
|
|
56
|
+
await showHelp();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function showHelp() {
|
|
60
|
+
const { createRequire } = await import('node:module');
|
|
61
|
+
const require = createRequire(import.meta.url);
|
|
62
|
+
const version = require('../package.json').version;
|
|
63
|
+
console.log(`throughline v${version}
|
|
64
|
+
|
|
65
|
+
Usage:
|
|
66
|
+
throughline install Register hooks in ~/.claude/settings.json
|
|
67
|
+
throughline uninstall Remove hooks
|
|
68
|
+
throughline monitor Multi-session token monitor (use --all, --session <id>)
|
|
69
|
+
throughline detail <time> Retrieve L2+L3 detail for a turn (e.g. 14:23:05 or 14:23-14:30)
|
|
70
|
+
throughline doctor Check environment
|
|
71
|
+
throughline status Show DB statistics
|
|
72
|
+
throughline --version Show version
|
|
73
|
+
|
|
74
|
+
Hook subcommands (called by Claude Code):
|
|
75
|
+
throughline session-start SessionStart hook
|
|
76
|
+
throughline process-turn Stop hook
|
|
77
|
+
`);
|
|
78
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "throughline",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Claude Code hooks plugin for structured context compression (/clear-safe persistent memory)",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"claude-code",
|
|
8
|
+
"hooks",
|
|
9
|
+
"context-compression",
|
|
10
|
+
"llm"
|
|
11
|
+
],
|
|
12
|
+
"author": "kitepon",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/kitepon-rgb/Throughline.git"
|
|
17
|
+
},
|
|
18
|
+
"bin": {
|
|
19
|
+
"throughline": "bin/throughline.mjs"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"bin/",
|
|
23
|
+
"src/",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "node --test src/*.test.mjs"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22.5"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* throughline doctor — 環境チェック
|
|
3
|
+
*
|
|
4
|
+
* チェック項目:
|
|
5
|
+
* - Node.js バージョン >= 22.5
|
|
6
|
+
* - node:sqlite が使えるか
|
|
7
|
+
* - ~/.throughline/throughline.db が書き込み可能か
|
|
8
|
+
* - ~/.claude/settings.json に Throughline hook が登録されているか
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, accessSync, readFileSync, constants } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { execSync } from 'node:child_process';
|
|
15
|
+
|
|
16
|
+
const GREEN = '\x1b[32m✓\x1b[0m';
|
|
17
|
+
const RED = '\x1b[31m✗\x1b[0m';
|
|
18
|
+
const YELLOW = '\x1b[33m!\x1b[0m';
|
|
19
|
+
|
|
20
|
+
async function check(label, fn) {
|
|
21
|
+
try {
|
|
22
|
+
const result = await fn();
|
|
23
|
+
if (result === false) {
|
|
24
|
+
console.log(`${YELLOW} ${label}`);
|
|
25
|
+
} else {
|
|
26
|
+
console.log(`${GREEN} ${label}${result ? ': ' + result : ''}`);
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.log(`${RED} ${label}: ${err.message}`);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function run() {
|
|
36
|
+
console.log('throughline doctor\n');
|
|
37
|
+
|
|
38
|
+
// Node.js バージョン
|
|
39
|
+
await check('Node.js >= 22.5', () => {
|
|
40
|
+
const [major, minor] = process.versions.node.split('.').map(Number);
|
|
41
|
+
if (major < 22 || (major === 22 && minor < 5)) {
|
|
42
|
+
throw new Error(`Node.js ${process.versions.node} — 22.5 以上が必要`);
|
|
43
|
+
}
|
|
44
|
+
return process.versions.node;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// node:sqlite
|
|
48
|
+
await check('node:sqlite が使えるか', async () => {
|
|
49
|
+
const { DatabaseSync } = await import('node:sqlite');
|
|
50
|
+
new DatabaseSync(':memory:').close();
|
|
51
|
+
return 'ok';
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// DB ディレクトリ
|
|
55
|
+
const dbDir = join(homedir(), '.throughline');
|
|
56
|
+
const dbPath = join(dbDir, 'throughline.db');
|
|
57
|
+
await check('~/.throughline/ ディレクトリ', () => {
|
|
58
|
+
if (!existsSync(dbDir)) throw new Error('ディレクトリが存在しない(初回実行前)');
|
|
59
|
+
accessSync(dbDir, constants.W_OK);
|
|
60
|
+
return dbDir;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// DB ファイル
|
|
64
|
+
await check('throughline.db', () => {
|
|
65
|
+
if (!existsSync(dbPath)) return false; // 未作成(初回前)
|
|
66
|
+
accessSync(dbPath, constants.W_OK);
|
|
67
|
+
return dbPath;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// hook 登録確認(グローバルまたはプロジェクトローカル)
|
|
71
|
+
const globalSettings = join(homedir(), '.claude', 'settings.json');
|
|
72
|
+
const localSettings = join(process.cwd(), '.claude', 'settings.json');
|
|
73
|
+
await check('Throughline hook が登録されているか', () => {
|
|
74
|
+
function hasHook(filePath) {
|
|
75
|
+
if (!existsSync(filePath)) return false;
|
|
76
|
+
const settings = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
77
|
+
return Object.values(settings.hooks ?? {}).flat().some(group =>
|
|
78
|
+
(group.hooks ?? []).some(h => h.command?.includes('throughline'))
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (hasHook(globalSettings)) return 'グローバル (~/.claude/settings.json)';
|
|
82
|
+
if (hasHook(localSettings)) return 'プロジェクトローカル (.claude/settings.json)';
|
|
83
|
+
throw new Error('登録なし — throughline install を実行してください');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// PATH 上に throughline があるか
|
|
87
|
+
await check('throughline コマンドが PATH で見つかるか', () => {
|
|
88
|
+
try {
|
|
89
|
+
const which = process.platform === 'win32' ? 'where throughline' : 'which throughline';
|
|
90
|
+
const result = execSync(which, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
91
|
+
return result.split(/\r?\n/)[0];
|
|
92
|
+
} catch {
|
|
93
|
+
throw new Error('見つからない — npm install -g throughline を実行してください');
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
console.log('');
|
|
98
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* throughline install / uninstall
|
|
4
|
+
*
|
|
5
|
+
* デフォルト: ~/.claude/settings.json(グローバル、全プロジェクトに適用)
|
|
6
|
+
* --project : .claude/settings.json(プロジェクトローカル)
|
|
7
|
+
* --uninstall: hook を削除
|
|
8
|
+
*
|
|
9
|
+
* 登録コマンドは PATH 解決型 (throughline <subcommand>) を使う。
|
|
10
|
+
* node のインストール先や OS が変わっても PATH さえ通れば動く。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
14
|
+
import { join, dirname } from 'node:path';
|
|
15
|
+
import { homedir } from 'node:os';
|
|
16
|
+
|
|
17
|
+
// Throughline が管理する hook コマンド一覧
|
|
18
|
+
// schema v4 以降: PostToolUse (capture-tool) は廃止。Stop 内で L2/L3 を一括処理する。
|
|
19
|
+
const SC_COMMANDS = [
|
|
20
|
+
'throughline process-turn',
|
|
21
|
+
'throughline session-start',
|
|
22
|
+
// 旧コマンド(アンインストール時に除去する)
|
|
23
|
+
'throughline inject-context',
|
|
24
|
+
'throughline capture-tool',
|
|
25
|
+
'node src/detail-capture.mjs',
|
|
26
|
+
'node src/classifier.mjs',
|
|
27
|
+
'node src/turn-processor.mjs',
|
|
28
|
+
'node src/context-injector.mjs',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const SC_HOOKS = {
|
|
32
|
+
SessionStart: {
|
|
33
|
+
hooks: [{ type: 'command', command: 'throughline session-start' }],
|
|
34
|
+
},
|
|
35
|
+
Stop: {
|
|
36
|
+
hooks: [{ type: 'command', command: 'throughline process-turn' }],
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function resolveSettingsPath(args) {
|
|
41
|
+
if (args.includes('--project')) {
|
|
42
|
+
return join(process.cwd(), '.claude', 'settings.json');
|
|
43
|
+
}
|
|
44
|
+
return join(homedir(), '.claude', 'settings.json');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readSettings(settingsPath) {
|
|
48
|
+
if (!existsSync(settingsPath)) return {};
|
|
49
|
+
return JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function writeSettings(settingsPath, obj) {
|
|
53
|
+
const dir = dirname(settingsPath);
|
|
54
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
55
|
+
writeFileSync(settingsPath, JSON.stringify(obj, null, 2) + '\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function run(args = []) {
|
|
59
|
+
const uninstall = args.includes('--uninstall');
|
|
60
|
+
const settingsPath = resolveSettingsPath(args);
|
|
61
|
+
const current = readSettings(settingsPath);
|
|
62
|
+
const existingHooks = current.hooks ?? {};
|
|
63
|
+
const scSet = new Set(SC_COMMANDS);
|
|
64
|
+
|
|
65
|
+
if (uninstall) {
|
|
66
|
+
for (const [key, groups] of Object.entries(existingHooks)) {
|
|
67
|
+
existingHooks[key] = groups.filter(group =>
|
|
68
|
+
!(group.hooks ?? []).some(h => scSet.has(h.command))
|
|
69
|
+
);
|
|
70
|
+
if (existingHooks[key].length === 0) delete existingHooks[key];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (Object.keys(existingHooks).length === 0) {
|
|
74
|
+
delete current.hooks;
|
|
75
|
+
} else {
|
|
76
|
+
current.hooks = existingHooks;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
writeSettings(settingsPath, current);
|
|
80
|
+
console.log('Throughline hooks を削除しました。');
|
|
81
|
+
console.log(` ${settingsPath}`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// インストール
|
|
86
|
+
for (const [key, entry] of Object.entries(SC_HOOKS)) {
|
|
87
|
+
const list = existingHooks[key] ?? [];
|
|
88
|
+
const cmd = entry.hooks[0].command;
|
|
89
|
+
const alreadyExists = list.some(group =>
|
|
90
|
+
(group.hooks ?? []).some(h => h.command === cmd)
|
|
91
|
+
);
|
|
92
|
+
if (!alreadyExists) {
|
|
93
|
+
existingHooks[key] = [entry, ...list];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
current.hooks = existingHooks;
|
|
98
|
+
writeSettings(settingsPath, current);
|
|
99
|
+
|
|
100
|
+
const scope = args.includes('--project') ? 'プロジェクトローカル' : 'グローバル(全プロジェクト)';
|
|
101
|
+
console.log(`Throughline hooks をインストールしました [${scope}]`);
|
|
102
|
+
console.log(` ${settingsPath}`);
|
|
103
|
+
console.log('');
|
|
104
|
+
console.log('有効な hooks:');
|
|
105
|
+
console.log(' SessionStart → throughline session-start (セッション記録・記憶張り替え・L1/L2 注入)');
|
|
106
|
+
console.log(' Stop → throughline process-turn (L1 要約 + L2 本文保存 + L3 詳細保存)');
|
|
107
|
+
console.log('');
|
|
108
|
+
console.log(' アンインストール: throughline uninstall');
|
|
109
|
+
}
|