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 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
+ }