kankaku-claude 0.9.0 → 0.11.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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +75 -7
- package/README.md +109 -17
- package/commands/doctor.md +1 -1
- package/commands/report.md +1 -1
- package/commands/setup.md +1 -1
- package/commands/status.md +1 -1
- package/commands/sync-all.md +1 -1
- package/commands/sync-status.md +1 -1
- package/commands/sync.md +3 -3
- package/dist/auto-sync.js +28 -0
- package/dist/claude-pid.js +61 -0
- package/dist/cli-core.js +78 -0
- package/{src/cli.ts → dist/cli.js} +11 -12
- package/dist/cost-store.js +121 -0
- package/dist/doctor.js +103 -0
- package/dist/event-log.js +31 -0
- package/dist/events.js +72 -0
- package/dist/handle-hook.js +292 -0
- package/dist/hook.js +38 -0
- package/dist/inflight-recovery.js +71 -0
- package/dist/paths.js +35 -0
- package/dist/prompts.js +33 -0
- package/dist/record.js +56 -0
- package/dist/replay.js +101 -0
- package/dist/report.js +102 -0
- package/dist/session-state.js +47 -0
- package/dist/statusline-core.js +71 -0
- package/dist/statusline.js +34 -0
- package/dist/sync-cli.js +100 -0
- package/dist/work-target.js +71 -0
- package/hooks/hooks.json +9 -9
- package/package.json +7 -6
- package/src/auto-sync.ts +0 -32
- package/src/claude-pid.ts +0 -68
- package/src/cli-core.ts +0 -103
- package/src/cost-store.ts +0 -138
- package/src/doctor.ts +0 -89
- package/src/event-log.ts +0 -33
- package/src/events.ts +0 -145
- package/src/handle-hook.ts +0 -299
- package/src/hook.ts +0 -41
- package/src/inflight-recovery.ts +0 -82
- package/src/paths.ts +0 -50
- package/src/prompts.ts +0 -42
- package/src/record.ts +0 -28
- package/src/replay.ts +0 -123
- package/src/report.ts +0 -132
- package/src/session-state.ts +0 -71
- package/src/statusline-core.ts +0 -89
- package/src/statusline.ts +0 -36
- package/src/sync-cli.ts +0 -122
- package/tsconfig.json +0 -14
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kankaku",
|
|
3
3
|
"description": "Records how long Claude Code works on each of your prompts: wall time, waiting time, work time and cost, per prompt, in kankaku's worklog format.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.11.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "soyunninja"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -4,16 +4,75 @@ All notable changes to this project are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
-
## 0.
|
|
7
|
+
## 0.11.0 — 2026-09-29
|
|
8
8
|
|
|
9
|
-
###
|
|
9
|
+
### Added
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
- **Records resolve their client and project automatically.** Records
|
|
12
|
+
written at Stop, SessionEnd and crash recovery now carry `clientId`,
|
|
13
|
+
`clientName`, `projectId` and `projectName` (and the legacy `client`
|
|
14
|
+
label) when the project `config.json` ids or the cached catalog's
|
|
15
|
+
`repo_paths` match the session's working directory, so the hub no longer
|
|
16
|
+
files them under the unassigned client. The catalog is read from
|
|
17
|
+
`~/.kankaku/catalog.json` only: no network, no delay, and no target when
|
|
18
|
+
the cache is missing. Records already on disk are not rewritten, and rows
|
|
19
|
+
already uploaded as unassigned stay so until reassigned in the web app.
|
|
20
|
+
- `/kankaku:status` and `/kankaku:doctor` print the resolved target and its
|
|
21
|
+
source, or `target: none (<reason>)`.
|
|
22
|
+
|
|
23
|
+
## 0.10.2 — 2026-09-29
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **Records synced by another tool were attributed to the syncer.** Records
|
|
28
|
+
carried no `agent`/`plugin`, so when the kankaku TUI synced a worklog
|
|
29
|
+
written by Claude Code, the hub row was created as agent `unknown`,
|
|
30
|
+
plugin `kankaku-tui`. Every record (settled, settled at SessionEnd, or
|
|
31
|
+
recovered as `interrupted`) now carries `agent: "claude-code"`,
|
|
32
|
+
`plugin: "kankaku-claude"` and `pluginVersion`. Records already on disk
|
|
33
|
+
are not rewritten.
|
|
34
|
+
|
|
35
|
+
## 0.10.1 — 2026-09-29
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- **A prompt interrupted by a hung session was recorded with the time until
|
|
40
|
+
the next session started.** Crash recovery settled the open prompt at
|
|
41
|
+
recovery time, so a session that hung and was recovered the next morning
|
|
42
|
+
produced a record with a 14.4 h wall time. The prompt is now closed at
|
|
43
|
+
the timestamp of the last event recorded for it (its own
|
|
44
|
+
`UserPromptSubmit` when nothing followed); a waiting span still open is
|
|
45
|
+
closed at the same instant, and the record stays `interrupted`.
|
|
15
46
|
|
|
16
|
-
##
|
|
47
|
+
## 0.10.0 — 2026-09-28
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- **Installed as a dependency, the plugin could not run at all.** This
|
|
52
|
+
package now compiles to `dist/` (`npm run build`, `tsc -p
|
|
53
|
+
tsconfig.build.json`) and ships `dist/` instead of `src/`/`tsconfig.json`
|
|
54
|
+
in `package.json`'s `files`. Every hook/statusline/CLI command
|
|
55
|
+
(`hooks/hooks.json`, `commands/*.md`, the `statusLine` snippet
|
|
56
|
+
`kankaku setup`/`/kankaku:setup` prints) now points at the compiled
|
|
57
|
+
`dist/*.js`, never `src/*.ts` — Node 24 refuses to type-strip a `.ts`
|
|
58
|
+
file once it sits under a `node_modules` directory
|
|
59
|
+
(`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`), so a `kankaku-tui`
|
|
60
|
+
install of this package (its dependency, not a checkout) could not run
|
|
61
|
+
a single hook, the statusline, or the CLI before this fix. `npm run
|
|
62
|
+
check`/`prepublishOnly` build first; a manual `--plugin-dir` checkout
|
|
63
|
+
now needs `npm install && npm run build` once (see README "Install").
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- README "Install": the recommended path is now `npm i -g kankaku-tui &&
|
|
68
|
+
kankaku setup` — `kankaku-tui` depends on this package directly and its
|
|
69
|
+
setup wizard/`--yes` flow writes both `statusLine` and `hooks` into
|
|
70
|
+
`~/.claude/settings.json` in one step, so no checkout or `--plugin-dir`
|
|
71
|
+
is required for the plugin to measure time or report cost.
|
|
72
|
+
`--plugin-dir <checkout>` remains documented as the manual/dev path
|
|
73
|
+
(also the only way to get the `/kankaku:*` slash commands), with a note
|
|
74
|
+
that combining it with a `kankaku setup`-configured machine double-runs
|
|
75
|
+
the hooks and double-writes worklog records.
|
|
17
76
|
|
|
18
77
|
### Added
|
|
19
78
|
|
|
@@ -71,3 +130,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
71
130
|
- `SessionEnd` also deletes the session's cost file; `SessionStart`
|
|
72
131
|
(non-`compact`) sweeps cost files older than 7 days.
|
|
73
132
|
- `node src/cli.ts status` reads cost from the cost file.
|
|
133
|
+
|
|
134
|
+
## 0.9.0 — 2026-09-28
|
|
135
|
+
|
|
136
|
+
### Changed
|
|
137
|
+
|
|
138
|
+
- Moved into the kankaku monorepo (`github.com/soyunninja/kankaku`,
|
|
139
|
+
`packages/claude`); versions are now lockstep with the other client
|
|
140
|
+
packages (`kankaku`, `kankaku-tui`). Depends on `kankaku ^0.9.0` (was
|
|
141
|
+
`^0.6.0`). No behaviour change.
|
package/README.md
CHANGED
|
@@ -25,25 +25,76 @@ its own `worklog.jsonl`. This means kankaku's existing report and export
|
|
|
25
25
|
tooling can read this plugin's worklog unchanged. kankaku-claude also exposes
|
|
26
26
|
manual and best-effort automatic hub sync through kankaku's public hub adapters (below).
|
|
27
27
|
|
|
28
|
+
Every record carries the identity of who measured it: `agent: "claude-code"`,
|
|
29
|
+
`plugin: "kankaku-claude"` and `pluginVersion` (this package's version).
|
|
30
|
+
A worklog synced by another tool, such as the kankaku TUI, therefore keeps
|
|
31
|
+
the right agent on the hub. `agentVersion` is left unset because Claude Code
|
|
32
|
+
does not pass its version to hooks. Records written before this version carry
|
|
33
|
+
no identity and are labelled by whichever tool syncs them first.
|
|
34
|
+
|
|
28
35
|
## Requirements
|
|
29
36
|
|
|
30
37
|
- Claude Code with plugin support.
|
|
31
|
-
- Node.js >= 24 on `PATH
|
|
32
|
-
|
|
38
|
+
- Node.js >= 24 on `PATH`. This package ships a compiled `dist/` (`npm
|
|
39
|
+
run build`, `tsc -p tsconfig.build.json`) — Node refuses to type-strip
|
|
40
|
+
a `.ts` file once it sits under a `node_modules` directory
|
|
41
|
+
(`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`), so every published
|
|
42
|
+
hook/statusline/CLI command runs the built `dist/*.js` file, never the
|
|
43
|
+
`.ts` source. Installed via `kankaku-tui` (below) this needs no action
|
|
44
|
+
from you; a manual `--plugin-dir` checkout must run `npm install && npm
|
|
45
|
+
run build` once before Claude Code can load it (see "Manual/dev"
|
|
46
|
+
below).
|
|
33
47
|
|
|
34
48
|
## Install
|
|
35
49
|
|
|
36
|
-
**
|
|
37
|
-
|
|
50
|
+
**Recommended: `kankaku-tui`'s setup wizard.** This package lives inside
|
|
51
|
+
`kankaku-tui`'s own dependency tree, so installing and running its setup
|
|
52
|
+
wizard configures Claude Code for you — statusline and hooks together, in
|
|
53
|
+
one step, with no checkout and no `--plugin-dir`:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm i -g kankaku-tui
|
|
57
|
+
kankaku setup
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Checking Claude Code in the wizard (or confirming it in `kankaku setup
|
|
61
|
+
--yes`) writes `statusLine` and `hooks` into `~/.claude/settings.json`,
|
|
62
|
+
resolved from this package's own bundled files, merged with — never
|
|
63
|
+
clobbering — whatever else is already there. See `kankaku-tui`'s own
|
|
64
|
+
README ("Claude Code") for the full behaviour, the
|
|
65
|
+
`--claude-plugin-dir`/`KANKAKU_CLAUDE_PLUGIN_DIR` override, and why you
|
|
66
|
+
should drop `--plugin-dir` (below) once this has run.
|
|
67
|
+
|
|
68
|
+
**Manual/dev: `--plugin-dir`.** Clone this repository, build it once, then point Claude Code
|
|
69
|
+
at it directly:
|
|
38
70
|
|
|
39
71
|
```bash
|
|
72
|
+
git clone https://github.com/soyunninja/kankaku.git
|
|
73
|
+
cd kankaku/packages/claude
|
|
74
|
+
npm install && npm run build
|
|
40
75
|
claude --plugin-dir /path/to/kankaku-claude
|
|
41
76
|
```
|
|
42
77
|
|
|
78
|
+
`npm run build` compiles `src/` into `dist/`; the hooks, statusline and
|
|
79
|
+
`/kankaku:*` commands all run `dist/*.js`, never `src/*.ts` — a checkout
|
|
80
|
+
that skips this step fails at the hook's very first launch with
|
|
81
|
+
`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING` (or, for `--plugin-dir`
|
|
82
|
+
itself, simply nothing recorded). Re-run `npm run build` after pulling
|
|
83
|
+
new commits.
|
|
84
|
+
|
|
43
85
|
If your Claude Code version does not recognize `--plugin-dir`, or plugin
|
|
44
86
|
loading has changed since this was written, check your installed version's
|
|
45
87
|
own plugin documentation (`claude --help`, or `/plugin` inside a session) for
|
|
46
|
-
the current local-install flow.
|
|
88
|
+
the current local-install flow. A plugin
|
|
89
|
+
wired only through settings.json hooks does not register its commands, so
|
|
90
|
+
`kankaku setup` installs the `/kankaku:*` commands separately (see
|
|
91
|
+
"Commands").
|
|
92
|
+
|
|
93
|
+
**Do not combine the two.** If `kankaku setup` has already configured this
|
|
94
|
+
machine's `~/.claude/settings.json` hooks, loading the plugin again with
|
|
95
|
+
`--plugin-dir` runs the same hooks twice per event and double-writes
|
|
96
|
+
worklog records. Use `--plugin-dir` only on a machine `kankaku setup` has
|
|
97
|
+
not touched, or drop it once setup has run.
|
|
47
98
|
|
|
48
99
|
**From a marketplace.** Once this plugin is published to a marketplace:
|
|
49
100
|
|
|
@@ -61,7 +112,8 @@ there never blocks the plugin from loading.
|
|
|
61
112
|
Claude Code plugins cannot set `statusLine` for themselves — there is no
|
|
62
113
|
programmatic way for a plugin to add a `statusLine` entry to your settings.
|
|
63
114
|
The statusline is also the *only* documented source of per-prompt cost
|
|
64
|
-
(`cost.total_cost_usd`); hooks never receive it.
|
|
115
|
+
(`cost.total_cost_usd`); hooks never receive it. `kankaku setup` (above)
|
|
116
|
+
writes it for you automatically; without `kankaku-tui`, run:
|
|
65
117
|
|
|
66
118
|
```
|
|
67
119
|
/kankaku:setup
|
|
@@ -81,23 +133,27 @@ anything behind in whatever project happens to be open.
|
|
|
81
133
|
|
|
82
134
|
## Commands
|
|
83
135
|
|
|
136
|
+
`kankaku setup` installs these as user commands under
|
|
137
|
+
`~/.claude/commands/kankaku/`, so they work without `--plugin-dir`.
|
|
138
|
+
|
|
84
139
|
- `/kankaku:report` — a report of recent work, grouped by day (wraps
|
|
85
|
-
`node
|
|
140
|
+
`node dist/cli.js report`).
|
|
86
141
|
- `/kankaku:status` — the sessions kankaku-claude currently has state for:
|
|
87
142
|
session id, whether its process is still alive, whether a prompt is open,
|
|
88
|
-
and the last cost the statusline reported
|
|
143
|
+
and the last cost the statusline reported, preceded by the resolved work
|
|
144
|
+
target (wraps `node dist/cli.js status`).
|
|
89
145
|
- `/kankaku:setup` — prints the `statusLine` snippet described above (wraps
|
|
90
|
-
`node
|
|
146
|
+
`node dist/cli.js setup`).
|
|
91
147
|
- `/kankaku:sync` — manually syncs recent local work records to the hub
|
|
92
|
-
(wraps `node
|
|
148
|
+
(wraps `node dist/cli.js sync`; the slash command does not forward arguments).
|
|
93
149
|
- `/kankaku:sync-status` — inspects local pending counts and sync state
|
|
94
|
-
(wraps `node
|
|
95
|
-
- `/kankaku:sync-all` — requests a full sync (wraps `node
|
|
150
|
+
(wraps `node dist/cli.js sync status`; no hub request or credentials required).
|
|
151
|
+
- `/kankaku:sync-all` — requests a full sync (wraps `node dist/cli.js sync all`).
|
|
96
152
|
- `/kankaku:doctor` — a read-only local diagnostic of plugin files, session and
|
|
97
|
-
cost visibility, and hub sync state (wraps `node
|
|
153
|
+
cost visibility, and hub sync state (wraps `node dist/cli.js doctor`).
|
|
98
154
|
|
|
99
155
|
The report, status, setup, and doctor subcommands are also available directly via
|
|
100
|
-
`node
|
|
156
|
+
`node dist/cli.js <report|status|setup|doctor>`; `report` accepts `--days N` and
|
|
101
157
|
defaults to the last 7 days. Doctor reads only local data; it never contacts the
|
|
102
158
|
hub or prints credentials. A missing cost file means cost has not been observed
|
|
103
159
|
under the current `HOME` (it does not prove the statusline is misconfigured).
|
|
@@ -114,9 +170,9 @@ request or credentials. The same operations are available directly via CLI:
|
|
|
114
170
|
|
|
115
171
|
| Command | Purpose |
|
|
116
172
|
|---------|---------|
|
|
117
|
-
| `node
|
|
118
|
-
| `node
|
|
119
|
-
| `node
|
|
173
|
+
| `node dist/cli.js sync` | Manually sync the recent window (24 hours by default). |
|
|
174
|
+
| `node dist/cli.js sync all` | Request a full sync. |
|
|
175
|
+
| `node dist/cli.js sync status` | Inspect local pending counts and sync state; no hub network request or credentials required. |
|
|
120
176
|
|
|
121
177
|
Optional environment settings:
|
|
122
178
|
|
|
@@ -140,6 +196,36 @@ window, and record settings above apply to both manual and automatic sync.
|
|
|
140
196
|
Automatic runs use kankaku's change detection and per-prompt throttle; session
|
|
141
197
|
boundaries are not throttled. Set `KANKAKU_SYNC_AUTO=0` to opt out.
|
|
142
198
|
|
|
199
|
+
### Client and project assignment
|
|
200
|
+
|
|
201
|
+
Each record is stamped with a hub client and project when one resolves, so
|
|
202
|
+
the hub files the task under the right client instead of "Sin determinar".
|
|
203
|
+
Sources, in order:
|
|
204
|
+
|
|
205
|
+
1. the project's `<KANKAKU_DIR>/config.json` ids (`clientId`, optional
|
|
206
|
+
`projectId`);
|
|
207
|
+
2. the cached catalog's `repo_paths`: the active project whose path equals
|
|
208
|
+
the session's working directory, or contains it.
|
|
209
|
+
|
|
210
|
+
Inactive clients and projects, and the "unassigned" client, are never used.
|
|
211
|
+
The catalog is read from the cache file `~/.kankaku/catalog.json` only; a
|
|
212
|
+
hook never fetches it and never waits on the network. Every real sync
|
|
213
|
+
refreshes that cache, and `kankaku catalog refresh` refreshes it on demand.
|
|
214
|
+
Without a readable cache (or without hub credentials, which name the hub the
|
|
215
|
+
cache belongs to) no target resolves and records stay unassigned, exactly as
|
|
216
|
+
before. The legacy `client` label is the client's code when it is a valid
|
|
217
|
+
label; without a hub target it comes from `KANKAKU_CLIENT`, then the
|
|
218
|
+
`client` in `config.json`.
|
|
219
|
+
|
|
220
|
+
The target is resolved when a record is written (`Stop`, `SessionEnd`,
|
|
221
|
+
crash recovery), never on the per-tool-call hooks. `/kankaku:status` and
|
|
222
|
+
`/kankaku:doctor` print `target: <client> · <project> (source: ...)`, or
|
|
223
|
+
`target: none (<reason>)`.
|
|
224
|
+
|
|
225
|
+
There is nothing to pick inside Claude Code yet. Assignment is create-only
|
|
226
|
+
on the hub: a row already uploaded as unassigned stays that way until it is
|
|
227
|
+
reassigned in the web app; a later sync does not move it.
|
|
228
|
+
|
|
143
229
|
## Where the files live
|
|
144
230
|
|
|
145
231
|
- `<KANKAKU_DIR>/worklog.jsonl` — the append-only log of settled records,
|
|
@@ -172,6 +258,12 @@ and appended as one `status: "interrupted"` record before its files are
|
|
|
172
258
|
deleted; a dead session with no open prompt just has its files deleted. This
|
|
173
259
|
also runs for the current session's own leftover state at `SessionEnd`.
|
|
174
260
|
|
|
261
|
+
An interrupted prompt is closed at its last recorded activity — the
|
|
262
|
+
timestamp of the last event logged for it, or its own start when nothing
|
|
263
|
+
followed — never at the moment of recovery, so a session that hung and was
|
|
264
|
+
recovered hours later does not report those hours as work. A waiting span
|
|
265
|
+
still open is closed at that same instant.
|
|
266
|
+
|
|
175
267
|
## Limitations
|
|
176
268
|
|
|
177
269
|
- **`turns` is always 1 per run.** Claude Code hooks give no way to observe
|
package/commands/doctor.md
CHANGED
package/commands/report.md
CHANGED
package/commands/setup.md
CHANGED
|
@@ -9,4 +9,4 @@ Then tell the user, briefly: paste the printed `statusLine` block into their
|
|
|
9
9
|
because a Claude Code plugin cannot set `statusLine` for itself — the
|
|
10
10
|
statusline is the only documented source of per-session cost.
|
|
11
11
|
|
|
12
|
-
!node "${CLAUDE_PLUGIN_ROOT}/
|
|
12
|
+
!node "${CLAUDE_PLUGIN_ROOT}/dist/cli.js" setup
|
package/commands/status.md
CHANGED
package/commands/sync-all.md
CHANGED
|
@@ -6,4 +6,4 @@ allowed-tools: Bash(node:*)
|
|
|
6
6
|
Run the kankaku CLI sync all command and show its output to the user verbatim,
|
|
7
7
|
inside a code block, with no summarizing or reformatting:
|
|
8
8
|
|
|
9
|
-
!node "${CLAUDE_PLUGIN_ROOT}/
|
|
9
|
+
!node "${CLAUDE_PLUGIN_ROOT}/dist/cli.js" sync all
|
package/commands/sync-status.md
CHANGED
|
@@ -6,4 +6,4 @@ allowed-tools: Bash(node:*)
|
|
|
6
6
|
Run the kankaku CLI sync status command and show its output to the user verbatim,
|
|
7
7
|
inside a code block, with no summarizing or reformatting:
|
|
8
8
|
|
|
9
|
-
!node "${CLAUDE_PLUGIN_ROOT}/
|
|
9
|
+
!node "${CLAUDE_PLUGIN_ROOT}/dist/cli.js" sync status
|
package/commands/sync.md
CHANGED
|
@@ -6,9 +6,9 @@ allowed-tools: Bash(node:*)
|
|
|
6
6
|
Run the kankaku CLI sync command and show its output to the user verbatim,
|
|
7
7
|
inside a code block, with no summarizing or reformatting:
|
|
8
8
|
|
|
9
|
-
!node "${CLAUDE_PLUGIN_ROOT}/
|
|
9
|
+
!node "${CLAUDE_PLUGIN_ROOT}/dist/cli.js" sync
|
|
10
10
|
|
|
11
11
|
For a full sync or local sync status, use the direct CLI instead:
|
|
12
|
-
`node "${CLAUDE_PLUGIN_ROOT}/
|
|
13
|
-
`node "${CLAUDE_PLUGIN_ROOT}/
|
|
12
|
+
`node "${CLAUDE_PLUGIN_ROOT}/dist/cli.js" sync all` or
|
|
13
|
+
`node "${CLAUDE_PLUGIN_ROOT}/dist/cli.js" sync status`.
|
|
14
14
|
This slash command runs the default sync only; it does not forward arguments.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Best-effort heavy-hook sync; credential/transport errors never escape into record handling. */
|
|
2
|
+
export async function autoSync(trigger, deps) {
|
|
3
|
+
if (deps.env.KANKAKU_SYNC_AUTO === "0")
|
|
4
|
+
return;
|
|
5
|
+
try {
|
|
6
|
+
const { resolveHubCredentials } = await import("kankaku/hub");
|
|
7
|
+
const { homedir } = await import("node:os");
|
|
8
|
+
const hub = resolveHubCredentials({ env: deps.env, homeDir: deps.homeDir ?? (() => deps.env.HOME || homedir()) });
|
|
9
|
+
if (!hub.credentials || hub.invalidReason)
|
|
10
|
+
return;
|
|
11
|
+
const { syncConfigured } = await import("./sync-cli.js");
|
|
12
|
+
// One deadline across catalog and upload requests; the public client also
|
|
13
|
+
// imposes its own per-request timeout. No timer persists after the hook.
|
|
14
|
+
const deadline = AbortSignal.timeout(8_000);
|
|
15
|
+
const doFetch = deps.fetch ?? fetch;
|
|
16
|
+
const boundedFetch = (input, init) => doFetch(input, {
|
|
17
|
+
...init,
|
|
18
|
+
signal: init?.signal ? AbortSignal.any([init.signal, deadline]) : deadline,
|
|
19
|
+
});
|
|
20
|
+
const result = await syncConfigured({ ...deps, fetch: boundedFetch }, hub.credentials, { trigger });
|
|
21
|
+
if (result.error || result.failed.length) {
|
|
22
|
+
deps.stderr(`kankaku auto-sync: ${result.error ?? result.failed.map((f) => f.reason).join("; ")}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
deps.stderr(`kankaku auto-sync: ${error instanceof Error ? error.message : String(error)}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
const SKIP_COMMS = new Set(["sh", "bash", "zsh", "dash", "fish", "node"]);
|
|
4
|
+
const MAX_HOPS = 6;
|
|
5
|
+
/**
|
|
6
|
+
* Walks ancestors from `startPid` (at most `MAX_HOPS` hops), returning the
|
|
7
|
+
* first pid whose `comm` basename is not a shell or `node` — the Claude
|
|
8
|
+
* Code process itself, skipping the shell/node layers a hook can be
|
|
9
|
+
* launched through. Falls back to `startPid` when `ps` fails or nothing
|
|
10
|
+
* qualifies within the hop budget.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveClaudePid({ startPid, runPs }) {
|
|
13
|
+
let pid = startPid;
|
|
14
|
+
for (let hop = 0; hop < MAX_HOPS; hop++) {
|
|
15
|
+
const info = runPs(pid);
|
|
16
|
+
if (!info)
|
|
17
|
+
return startPid;
|
|
18
|
+
if (!SKIP_COMMS.has(basename(info.comm)))
|
|
19
|
+
return pid;
|
|
20
|
+
pid = info.ppid;
|
|
21
|
+
}
|
|
22
|
+
return startPid;
|
|
23
|
+
}
|
|
24
|
+
/** Production `runPs`: `ps -o ppid=,comm= -p <pid>`, bounded to 200ms. */
|
|
25
|
+
export function runPsProcess(pid) {
|
|
26
|
+
try {
|
|
27
|
+
const out = execFileSync("ps", ["-o", "ppid=,comm=", "-p", String(pid)], {
|
|
28
|
+
timeout: 200,
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
});
|
|
31
|
+
const match = out.trim().match(/^(\d+)\s+(.+)$/);
|
|
32
|
+
if (!match)
|
|
33
|
+
return undefined;
|
|
34
|
+
const ppid = Number(match[1]);
|
|
35
|
+
const comm = match[2]?.trim();
|
|
36
|
+
if (!Number.isFinite(ppid) || !comm)
|
|
37
|
+
return undefined;
|
|
38
|
+
return { ppid, comm };
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* `true` unless `process.kill(pid, 0)` throws with a code other than
|
|
46
|
+
* `EPERM`. A non-positive or non-integer pid is always `false`, without
|
|
47
|
+
* calling `process.kill` at all: `pid` 0 signals the whole process GROUP
|
|
48
|
+
* (T7 — the defect that let a placeholder state file's `pid: 0` read back
|
|
49
|
+
* as alive forever).
|
|
50
|
+
*/
|
|
51
|
+
export function isAlive(pid) {
|
|
52
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
53
|
+
return false;
|
|
54
|
+
try {
|
|
55
|
+
process.kill(pid, 0);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
return error.code === "EPERM";
|
|
60
|
+
}
|
|
61
|
+
}
|
package/dist/cli-core.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { JsonlWorkLog } from "kankaku/hub";
|
|
4
|
+
import { listStateFiles, resolveKankakuDir } from "./paths.js";
|
|
5
|
+
import { readState } from "./session-state.js";
|
|
6
|
+
import { readCost } from "./cost-store.js";
|
|
7
|
+
import { formatReport } from "./report.js";
|
|
8
|
+
import { runSyncCli } from "./sync-cli.js";
|
|
9
|
+
import { runDoctor } from "./doctor.js";
|
|
10
|
+
import { formatTargetLine, resolveClaudeWorkTarget } from "./work-target.js";
|
|
11
|
+
const USAGE = "usage: node dist/cli.js <report|status|setup|sync|doctor> [--days N]\n";
|
|
12
|
+
/** CLI commands, resolved from `deps.cwd`. */
|
|
13
|
+
export async function runCli(argv, deps) {
|
|
14
|
+
const [command, ...rest] = argv;
|
|
15
|
+
switch (command) {
|
|
16
|
+
case "report":
|
|
17
|
+
return runReport(rest, deps);
|
|
18
|
+
case "status":
|
|
19
|
+
return runStatus(deps);
|
|
20
|
+
case "setup":
|
|
21
|
+
return runSetup(deps);
|
|
22
|
+
case "sync":
|
|
23
|
+
return runSyncCli(rest, deps);
|
|
24
|
+
case "doctor":
|
|
25
|
+
return { stdout: runDoctor(deps), exitCode: 0 };
|
|
26
|
+
default:
|
|
27
|
+
return { stdout: "", exitCode: 1, stderr: USAGE };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function runReport(args, deps) {
|
|
31
|
+
const days = parseDays(args);
|
|
32
|
+
const kankakuDir = resolveKankakuDir(deps.env.KANKAKU_DIR ?? ".kankaku", deps.cwd);
|
|
33
|
+
const records = new JsonlWorkLog(kankakuDir).readAll();
|
|
34
|
+
const stdout = formatReport(records, { now: deps.now(), ...(days !== undefined ? { days } : {}) });
|
|
35
|
+
return { stdout, exitCode: 0 };
|
|
36
|
+
}
|
|
37
|
+
function parseDays(args) {
|
|
38
|
+
const idx = args.indexOf("--days");
|
|
39
|
+
if (idx === -1)
|
|
40
|
+
return undefined;
|
|
41
|
+
const value = Number(args[idx + 1]);
|
|
42
|
+
return Number.isFinite(value) && value > 0 ? value : undefined;
|
|
43
|
+
}
|
|
44
|
+
function runStatus(deps) {
|
|
45
|
+
const kankakuDir = resolveKankakuDir(deps.env.KANKAKU_DIR ?? ".kankaku", deps.cwd);
|
|
46
|
+
const claudeDir = join(kankakuDir, "claude");
|
|
47
|
+
const files = listStateFiles(claudeDir);
|
|
48
|
+
const targetLine = formatTargetLine(resolveClaudeWorkTarget({ cwd: deps.cwd, kankakuDir, homeDir: deps.env.HOME || homedir(), env: deps.env }));
|
|
49
|
+
if (files.length === 0) {
|
|
50
|
+
return { stdout: `${targetLine}\nNo active sessions.\n`, exitCode: 0 };
|
|
51
|
+
}
|
|
52
|
+
const lines = files
|
|
53
|
+
.sort()
|
|
54
|
+
.map((file) => formatStatusLine(sessionIdFromStateFile(file), file, deps.isAlive, deps.env));
|
|
55
|
+
return { stdout: [targetLine, ...lines].join("\n") + "\n", exitCode: 0 };
|
|
56
|
+
}
|
|
57
|
+
function formatStatusLine(sessionId, stateFile, isAlive, env) {
|
|
58
|
+
const state = readState(stateFile);
|
|
59
|
+
if (!state)
|
|
60
|
+
return `${sessionId} (unreadable state)`;
|
|
61
|
+
const aliveWord = isAlive(state.pid) ? "alive" : "dead";
|
|
62
|
+
const promptWord = state.promptOpen
|
|
63
|
+
? `prompt open since ${new Date(state.promptOpen.startedAt).toISOString()}`
|
|
64
|
+
: "idle";
|
|
65
|
+
const cost = readCost(env, sessionId);
|
|
66
|
+
const costWord = cost ? `$${cost.totalUsd.toFixed(2)}` : "-";
|
|
67
|
+
return `${sessionId} pid ${state.pid} (${aliveWord}) ${promptWord} cost ${costWord}`;
|
|
68
|
+
}
|
|
69
|
+
function sessionIdFromStateFile(file) {
|
|
70
|
+
return basename(file).replace(/\.state\.json$/, "");
|
|
71
|
+
}
|
|
72
|
+
function runSetup(deps) {
|
|
73
|
+
const statuslinePath = join(deps.pluginRoot, "dist", "statusline.js");
|
|
74
|
+
const command = `node "${statuslinePath}"`;
|
|
75
|
+
const snippet = JSON.stringify({ statusLine: { type: "command", command } }, null, 2);
|
|
76
|
+
const explanation = "Claude Code plugins cannot set statusLine themselves, so this must be added to ~/.claude/settings.json manually: the statusline is the only documented source of per-session cost.";
|
|
77
|
+
return { stdout: `${snippet}\n\n${explanation}\n`, exitCode: 0 };
|
|
78
|
+
}
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
|
-
import { runCli } from "./cli-core.
|
|
4
|
-
import { isAlive } from "./claude-pid.
|
|
5
|
-
|
|
3
|
+
import { runCli } from "./cli-core.js";
|
|
4
|
+
import { isAlive } from "./claude-pid.js";
|
|
6
5
|
// src/cli.ts -> src -> repo root (the directory that contains .claude-plugin/).
|
|
7
6
|
const pluginRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
8
|
-
|
|
9
7
|
const result = await runCli(process.argv.slice(2), {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
env: process.env,
|
|
9
|
+
cwd: process.cwd(),
|
|
10
|
+
now: () => Date.now(),
|
|
11
|
+
isAlive,
|
|
12
|
+
pluginRoot,
|
|
15
13
|
});
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (result.stderr)
|
|
14
|
+
if (result.stdout)
|
|
15
|
+
process.stdout.write(result.stdout);
|
|
16
|
+
if (result.stderr)
|
|
17
|
+
process.stderr.write(result.stderr);
|
|
19
18
|
process.exit(result.exitCode);
|