kankaku-claude 0.12.1 → 1.0.1

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.
@@ -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.12.1",
4
+ "version": "1.0.1",
5
5
  "author": {
6
6
  "name": "soyunninja"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -4,6 +4,32 @@ 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
+ ## 1.0.1 — 2026-09-29
8
+
9
+ ### Fixed
10
+
11
+ - Spend that landed between two prompts was in no record: on a real session
12
+ the statusline total was 461.06 USD while its 134 records summed to
13
+ 299.80 USD, so 35 % of the spend was missing. A prompt's cost was measured
14
+ from the statusline snapshot at submit, which left background subagent
15
+ work and late statusline refreshes after a `Stop` unattributed. The
16
+ session state now keeps `costBaseline`, the total at the last settle, and
17
+ each prompt is measured from it (Stop, SessionEnd and crash recovery), so
18
+ the records of a session add up to its total. A new session starts at 0; a
19
+ counter reset records the new total. `/kankaku:status` shows `recorded $X
20
+ of $Y` and flags an unrecorded gap. Headless runs still have no cost.
21
+
22
+ ## 1.0.0 — 2026-09-29
23
+
24
+ ### Changed
25
+
26
+ - The library dependency is renamed from `kankaku` to `kankaku-pi`, and
27
+ its entry points are imported as `kankaku-pi/domain`, `kankaku-pi/ports`
28
+ and `kankaku-pi/hub`. The package keeps its own name, `kankaku-claude`
29
+ (directory unchanged). No behaviour change.
30
+ - `kankaku-claude sync` usage text names `dist/cli.js`, and comments no
31
+ longer point at files that are not in the repository.
32
+
7
33
  ## 0.12.1 — 2026-09-29
8
34
 
9
35
  No changes in this package; released in lockstep with kankaku 0.12.1.
package/README.md CHANGED
@@ -40,27 +40,27 @@ no identity and are labelled by whichever tool syncs them first.
40
40
  a `.ts` file once it sits under a `node_modules` directory
41
41
  (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`), so every published
42
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
43
+ `.ts` source. Installed via `kankaku` (below) this needs no action
44
44
  from you; a manual `--plugin-dir` checkout must run `npm install && npm
45
45
  run build` once before Claude Code can load it (see "Manual/dev"
46
46
  below).
47
47
 
48
48
  ## Install
49
49
 
50
- **Recommended: `kankaku-tui`'s setup wizard.** This package lives inside
51
- `kankaku-tui`'s own dependency tree, so installing and running its setup
50
+ **Recommended: `kankaku`'s setup wizard.** This package lives inside
51
+ `kankaku`'s own dependency tree, so installing and running its setup
52
52
  wizard configures Claude Code for you — statusline and hooks together, in
53
53
  one step, with no checkout and no `--plugin-dir`:
54
54
 
55
55
  ```bash
56
- npm i -g kankaku-tui
56
+ npm i -g kankaku
57
57
  kankaku setup
58
58
  ```
59
59
 
60
60
  Checking Claude Code in the wizard (or confirming it in `kankaku setup
61
61
  --yes`) writes `statusLine` and `hooks` into `~/.claude/settings.json`,
62
62
  resolved from this package's own bundled files, merged with — never
63
- clobbering — whatever else is already there. See `kankaku-tui`'s own
63
+ clobbering — whatever else is already there. See `kankaku`'s own
64
64
  README ("Claude Code") for the full behaviour, the
65
65
  `--claude-plugin-dir`/`KANKAKU_CLAUDE_PLUGIN_DIR` override, and why you
66
66
  should drop `--plugin-dir` (below) once this has run.
@@ -72,7 +72,7 @@ at it directly:
72
72
  git clone https://github.com/soyunninja/kankaku.git
73
73
  cd kankaku/packages/claude
74
74
  npm install && npm run build
75
- claude --plugin-dir /path/to/kankaku-claude
75
+ claude --plugin-dir "$PWD"
76
76
  ```
77
77
 
78
78
  `npm run build` compiles `src/` into `dist/`; the hooks, statusline and
@@ -113,7 +113,7 @@ Claude Code plugins cannot set `statusLine` for themselves — there is no
113
113
  programmatic way for a plugin to add a `statusLine` entry to your settings.
114
114
  The statusline is also the *only* documented source of per-prompt cost
115
115
  (`cost.total_cost_usd`); hooks never receive it. `kankaku setup` (above)
116
- writes it for you automatically; without `kankaku-tui`, run:
116
+ writes it for you automatically; without `kankaku`, run:
117
117
 
118
118
  ```
119
119
  /kankaku:setup
@@ -140,8 +140,11 @@ anything behind in whatever project happens to be open.
140
140
  `node dist/cli.js report`).
141
141
  - `/kankaku:status` — the sessions kankaku-claude currently has state for:
142
142
  session id, whether its process is still alive, whether a prompt is open,
143
- and the last cost the statusline reported, preceded by the resolved work
144
- target (wraps `node dist/cli.js status`).
143
+ and the last cost the statusline reported, followed by `recorded $X of
144
+ $Y` (the sum of this session's record costs in this project's worklog
145
+ against the session total). When the two differ by more than a cent and no
146
+ prompt is open, the line adds `(unrecorded $Z, goes to the next prompt)`.
147
+ The resolved work target comes first (wraps `node dist/cli.js status`).
145
148
  - `/kankaku:task` — links this session to a hub task (wraps
146
149
  `node dist/cli.js task`); see "Linking a task" below.
147
150
  - `/kankaku:setup` — prints the `statusLine` snippet described above (wraps
@@ -257,6 +260,37 @@ from Claude Code is not supported yet. Assignment is create-only
257
260
  on the hub: a row already uploaded as unassigned stays that way until it is
258
261
  reassigned in the web app; a later sync does not move it.
259
262
 
263
+ ## How cost is derived
264
+
265
+ Claude Code hooks carry no cost. The only source is the statusline, whose
266
+ `cost.total_cost_usd` is the running total of the session; the statusline
267
+ command stores the latest value under your home directory (see "Where the
268
+ files live").
269
+
270
+ - **Per-prompt difference.** A prompt's cost is the session total when the
271
+ prompt settles minus the total the prompt is measured from, rounded to
272
+ micro-dollars and never negative.
273
+ - **Chained baseline.** The session state keeps `costBaseline`, the session
274
+ total at the last settle. The next prompt is measured from that baseline,
275
+ not from the snapshot at submit, so the records of a session add up to its
276
+ total. A session Claude Code reports as newly started (`SessionStart`
277
+ source `startup`) begins with baseline 0. A resumed session keeps the
278
+ baseline of its state file; with no state file its first prompt is
279
+ measured from the snapshot at submit.
280
+ - **Spend between prompts belongs to the next record.** `worklog.jsonl` is
281
+ append-only and the previous record is already written, so anything spent
282
+ after a settle and before the next prompt (background subagents that keep
283
+ running, a statusline refresh that arrives after `Stop`) is added to the
284
+ next record of the same session.
285
+ - **Counter reset.** If the total is lower than the value the prompt is
286
+ measured from, the counter was reset: the prompt's cost is the new total
287
+ and the baseline restarts from it.
288
+ - **Headless runs have no cost.** `claude -p` renders no statusline, so
289
+ there is no total: the record stays without cost (`costObserved` unset)
290
+ and the baseline does not move.
291
+ - `/kankaku:status` shows `recorded $X of $Y` per live session so a gap is
292
+ visible.
293
+
260
294
  ## Where the files live
261
295
 
262
296
  - `<KANKAKU_DIR>/worklog.jsonl` — the append-only log of settled records,
@@ -304,9 +338,11 @@ still open is closed at that same instant.
304
338
  only the statusline's aggregate `total_cost_usd`; per-record `usage` token
305
339
  fields stay at zero, cost is the only populated figure.
306
340
  - **Cost is a per-prompt delta of the session total, from the statusline,
307
- and needs the manual setup step.** A tiny race is possible: the statusline
308
- can render after `Stop` has already computed the delta, in which case a
309
- sliver of one prompt's cost is attributed to the next prompt instead.
341
+ and needs the manual setup step.** See "How cost is derived". Spend that
342
+ lands between two prompts (a late statusline refresh, a background
343
+ subagent still running) is attributed to the next record of the session,
344
+ not lost. Headless `claude -p` runs have no statusline, so their records
345
+ carry no cost.
310
346
  - **A permission wait ends at the next hook event, not when you actually
311
347
  click.** There is no documented hook that fires the moment you answer a
312
348
  permission dialog, so the waiting span closes at whatever hook fires next
package/dist/auto-sync.js CHANGED
@@ -3,7 +3,7 @@ export async function autoSync(trigger, deps) {
3
3
  if (deps.env.KANKAKU_SYNC_AUTO === "0")
4
4
  return;
5
5
  try {
6
- const { resolveHubCredentials } = await import("kankaku/hub");
6
+ const { resolveHubCredentials } = await import("kankaku-pi/hub");
7
7
  const { homedir } = await import("node:os");
8
8
  const hub = resolveHubCredentials({ env: deps.env, homeDir: deps.homeDir ?? (() => deps.env.HOME || homedir()) });
9
9
  if (!hub.credentials || hub.invalidReason)
package/dist/cli-core.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { basename, join } from "node:path";
2
- import { JsonlWorkLog } from "kankaku/hub";
2
+ import { JsonlWorkLog } from "kankaku-pi/hub";
3
3
  import { listStateFiles, resolveKankakuDir } from "./paths.js";
4
4
  import { readState } from "./session-state.js";
5
5
  import { readCost } from "./cost-store.js";
@@ -52,12 +52,21 @@ function runStatus(deps) {
52
52
  if (files.length === 0) {
53
53
  return { stdout: `${targetLine}\nNo active sessions.\n`, exitCode: 0 };
54
54
  }
55
+ const recordedBySession = new Map();
56
+ for (const record of new JsonlWorkLog(kankakuDir).readAll()) {
57
+ if (record.sessionId === undefined)
58
+ continue;
59
+ recordedBySession.set(record.sessionId, (recordedBySession.get(record.sessionId) ?? 0) + record.usage.cost);
60
+ }
55
61
  const lines = files
56
62
  .sort()
57
- .map((file) => formatStatusLine(sessionIdFromStateFile(file), file, deps.isAlive, deps.env));
63
+ .map((file) => {
64
+ const sessionId = sessionIdFromStateFile(file);
65
+ return formatStatusLine(sessionId, file, deps.isAlive, deps.env, recordedBySession.get(sessionId) ?? 0);
66
+ });
58
67
  return { stdout: [targetLine, ...lines].join("\n") + "\n", exitCode: 0 };
59
68
  }
60
- function formatStatusLine(sessionId, stateFile, isAlive, env) {
69
+ function formatStatusLine(sessionId, stateFile, isAlive, env, recordedUsd) {
61
70
  const state = readState(stateFile);
62
71
  if (!state)
63
72
  return `${sessionId} (unreadable state)`;
@@ -67,7 +76,12 @@ function formatStatusLine(sessionId, stateFile, isAlive, env) {
67
76
  : "idle";
68
77
  const cost = readCost(env, sessionId);
69
78
  const costWord = cost ? `$${cost.totalUsd.toFixed(2)}` : "-";
70
- return `${sessionId} pid ${state.pid} (${aliveWord}) ${promptWord} cost ${costWord}`;
79
+ const base = `${sessionId} pid ${state.pid} (${aliveWord}) ${promptWord} cost ${costWord}`;
80
+ if (!cost)
81
+ return base;
82
+ const gap = cost.totalUsd - recordedUsd;
83
+ const warning = gap > 0.01 && !state.promptOpen ? ` (unrecorded $${gap.toFixed(2)}, goes to the next prompt)` : "";
84
+ return `${base} recorded $${recordedUsd.toFixed(2)} of $${cost.totalUsd.toFixed(2)}${warning}`;
71
85
  }
72
86
  function sessionIdFromStateFile(file) {
73
87
  return basename(file).replace(/\.state\.json$/, "");
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The chained per-prompt cost rule. `totalUsd` is the session total at
3
+ * settle, `start` the total the prompt is measured from (the previous
4
+ * settle's baseline, or the snapshot at submit for a session with none).
5
+ *
6
+ * - No finite total (headless, no statusline): nothing observed, baseline
7
+ * untouched.
8
+ * - Total below the start: the counter was reset, so the cost is the total.
9
+ * - Otherwise cost = total - start, in micro-dollars (no binary float noise).
10
+ *
11
+ * Node builtins only; safe on the light hook path.
12
+ */
13
+ export function settleCost(totalUsd, start) {
14
+ if (typeof totalUsd !== "number" || !Number.isFinite(totalUsd))
15
+ return {};
16
+ if (typeof start !== "number" || !Number.isFinite(start))
17
+ return { baseline: totalUsd };
18
+ const raw = totalUsd < start ? totalUsd : totalUsd - start;
19
+ return { cost: Math.max(0, Math.round(raw * 1e6) / 1e6), baseline: totalUsd };
20
+ }
@@ -6,9 +6,10 @@ const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
6
6
  * Cost lives under the HOME directory, never a project: the statusline
7
7
  * command is wired globally and runs in every session on the machine, so a
8
8
  * project-relative path would litter whatever project happens to be open
9
- * (the defect this module fixes — see `odd/tasks/hook-tracking.md` T7).
9
+ * (the defect this module fixes: cost files were written into whichever
10
+ * project happened to be open).
10
11
  * Imports only node builtins so light hooks that read/write cost stay off
11
- * the `kankaku` import path.
12
+ * the `kankaku-pi` import path.
12
13
  */
13
14
  export function costDir(env) {
14
15
  const home = env.HOME || homedir();
package/dist/doctor.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync, readdirSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { JsonlWorkLog, SyncStateStore, computeSyncStatus, resolveHubCredentials } from "kankaku/hub";
4
+ import { JsonlWorkLog, SyncStateStore, computeSyncStatus, resolveHubCredentials } from "kankaku-pi/hub";
5
5
  import { costDir } from "./cost-store.js";
6
6
  import { listStateFiles, resolveKankakuDir } from "./paths.js";
7
7
  import { readState } from "./session-state.js";
@@ -5,12 +5,13 @@ import { readState, writeState, updateState } from "./session-state.js";
5
5
  import { resolveClaudePid } from "./claude-pid.js";
6
6
  import { splitPrompts } from "./prompts.js";
7
7
  import { readCost, deleteCost, sweepStaleCostFiles } from "./cost-store.js";
8
+ import { settleCost } from "./cost-chain.js";
8
9
  const STOP_COST_WAIT_POLL_MS = 100;
9
10
  const STOP_COST_WAIT_MAX_MS = 1500;
10
11
  /**
11
12
  * Dispatches one Claude Code hook invocation. Light events (everything but
12
13
  * Stop/SessionStart/SessionEnd) only append one event line plus a tiny
13
- * state update, and never import `kankaku` at runtime — the heavier replay/
14
+ * state update, and never import `kankaku-pi` at runtime — the heavier replay/
14
15
  * storage layer is loaded with dynamic `import()` only inside the three
15
16
  * branches that actually settle or recover a record.
16
17
  */
@@ -31,14 +32,16 @@ export async function handleHook(input, deps) {
31
32
  ? { ts, event: "UserPromptSubmit", prompt, promptId }
32
33
  : { ts, event: "UserPromptSubmit", prompt };
33
34
  appendEvent(paths.eventsFile, event);
34
- const costAtStart = readCost(deps.env, sessionId)?.totalUsd;
35
+ const snapshot = readCost(deps.env, sessionId)?.totalUsd;
35
36
  updateState(paths.stateFile, (state) => ({
36
37
  pid: state?.pid ?? 0,
37
38
  parentPid: state?.parentPid ?? 0,
38
39
  cwd: state?.cwd || cwd,
39
40
  startedAt: state?.startedAt ?? ts,
40
- promptOpen: { id: promptId ?? `${sessionId}:${ts}`, startedAt: ts, costAtStart },
41
+ // The chained baseline wins over the snapshot: spend since the last settle belongs to this prompt.
42
+ promptOpen: { id: promptId ?? `${sessionId}:${ts}`, startedAt: ts, costAtStart: state?.costBaseline ?? snapshot },
41
43
  permissionOpen: null,
44
+ ...(state?.costBaseline !== undefined ? { costBaseline: state.costBaseline } : {}),
42
45
  }));
43
46
  return;
44
47
  }
@@ -68,6 +71,7 @@ export async function handleHook(input, deps) {
68
71
  startedAt: state?.startedAt ?? ts,
69
72
  promptOpen: state?.promptOpen ?? null,
70
73
  permissionOpen: ts,
74
+ ...(state?.costBaseline !== undefined ? { costBaseline: state.costBaseline } : {}),
71
75
  }));
72
76
  return;
73
77
  }
@@ -95,7 +99,7 @@ export async function handleHook(input, deps) {
95
99
  case "SessionStart": {
96
100
  const source = readString(raw.source) ?? "startup";
97
101
  appendEvent(paths.eventsFile, { ts, event: "SessionStart", source });
98
- await handleSessionStart(paths, sessionId, cwd, source, ts, deps);
102
+ await handleSessionStart(paths, sessionId, cwd, source, raw.source === "startup", ts, deps);
99
103
  return;
100
104
  }
101
105
  case "SessionEnd": {
@@ -111,7 +115,7 @@ export async function handleHook(input, deps) {
111
115
  async function handleStop(paths, sessionId, deps) {
112
116
  const { replayPrompt } = await import("./replay.js");
113
117
  const { buildClaudeRecord } = await import("./record.js");
114
- const { JsonlWorkLog } = await import("kankaku/hub");
118
+ const { JsonlWorkLog } = await import("kankaku-pi/hub");
115
119
  const events = readEventLog(paths.eventsFile);
116
120
  const beforeStopTs = events.length >= 2 ? events[events.length - 2].ts : events[events.length - 1]?.ts ?? deps.now();
117
121
  const deadline = deps.now() + STOP_COST_WAIT_MAX_MS;
@@ -128,28 +132,30 @@ async function handleStop(paths, sessionId, deps) {
128
132
  deps.stderr(`kankaku: no session state at Stop for ${sessionId}`);
129
133
  return;
130
134
  }
131
- const costAtStart = state.promptOpen?.costAtStart;
132
- const totalUsd = cost?.totalUsd;
133
- const costDelta = typeof costAtStart === "number" && typeof totalUsd === "number"
134
- ? Math.max(0, Math.round((totalUsd - costAtStart) * 1e6) / 1e6) // micro-dollars: no binary float noise in the record
135
- : undefined;
136
- const core = replayPrompt(last, { cost: costDelta });
135
+ // A Stop with no open prompt settles nothing: no cost, no baseline move.
136
+ const settled = state.promptOpen ? settleCost(cost?.totalUsd, state.promptOpen.costAtStart) : {};
137
+ const core = replayPrompt(last, { cost: settled.cost });
137
138
  if (core) {
138
139
  const assignment = await assignmentResolver(paths, deps);
139
140
  const record = buildClaudeRecord(core, state, sessionId, cost?.model, assignment(state.cwd, sessionId));
140
141
  const log = deps.log ?? new JsonlWorkLog(paths.kankakuDir);
141
142
  log.append(record);
142
143
  }
143
- writeState(paths.stateFile, { ...state, promptOpen: null, permissionOpen: null });
144
+ writeState(paths.stateFile, {
145
+ ...state,
146
+ promptOpen: null,
147
+ permissionOpen: null,
148
+ ...(settled.baseline !== undefined ? { costBaseline: settled.baseline } : {}),
149
+ });
144
150
  const keep = events.slice(0, events.length - last.events.length);
145
151
  dropSettledPrompts(paths.eventsFile, keep);
146
152
  if (core)
147
153
  await syncHeavy("agent_settled", state.cwd, deps);
148
154
  }
149
- async function handleSessionStart(paths, sessionId, cwd, source, ts, deps) {
155
+ async function handleSessionStart(paths, sessionId, cwd, source, isStartup, ts, deps) {
150
156
  if (source !== "compact") {
151
157
  const { recoverStaleSessions } = await import("./inflight-recovery.js");
152
- const { JsonlWorkLog } = await import("kankaku/hub");
158
+ const { JsonlWorkLog } = await import("kankaku-pi/hub");
153
159
  const recovered = recoverStaleSessions({
154
160
  claudeDir: paths.claudeDir,
155
161
  currentSessionId: sessionId,
@@ -180,6 +186,8 @@ async function handleSessionStart(paths, sessionId, cwd, source, ts, deps) {
180
186
  startedAt: ts,
181
187
  promptOpen: null,
182
188
  permissionOpen: null,
189
+ // A newly started session's counter starts at zero; any other source keeps the snapshot-at-submit fallback.
190
+ ...(isStartup ? { costBaseline: 0 } : {}),
183
191
  };
184
192
  writeState(paths.stateFile, state);
185
193
  await syncHeavy("session_start", cwd, deps);
@@ -189,14 +197,16 @@ async function handleSessionEnd(paths, sessionId, cwd, ts, deps) {
189
197
  if (state?.promptOpen) {
190
198
  const { replayPrompt } = await import("./replay.js");
191
199
  const { buildClaudeRecord } = await import("./record.js");
192
- const { JsonlWorkLog } = await import("kankaku/hub");
200
+ const { JsonlWorkLog } = await import("kankaku-pi/hub");
193
201
  const events = readEventLog(paths.eventsFile);
194
202
  const prompts = splitPrompts(events);
195
203
  const last = prompts[prompts.length - 1];
196
204
  if (last) {
197
- const core = replayPrompt(last, { settledAt: ts });
205
+ const costNow = readCost(deps.env, sessionId);
206
+ const settled = settleCost(costNow?.totalUsd, state.promptOpen.costAtStart);
207
+ const core = replayPrompt(last, { settledAt: ts, cost: settled.cost });
198
208
  if (core) {
199
- const model = readCost(deps.env, sessionId)?.model;
209
+ const model = costNow?.model;
200
210
  const assignment = await assignmentResolver(paths, deps);
201
211
  const record = buildClaudeRecord(core, state, sessionId, model, assignment(state.cwd, sessionId));
202
212
  const log = deps.log ?? new JsonlWorkLog(paths.kankakuDir);
@@ -6,6 +6,7 @@ import { readEventLog } from "./event-log.js";
6
6
  import { splitPrompts, replayPrompt } from "./replay.js";
7
7
  import { buildClaudeRecord } from "./record.js";
8
8
  import { readCost, deleteCost } from "./cost-store.js";
9
+ import { settleCost } from "./cost-chain.js";
9
10
  /**
10
11
  * Crash recovery: for every other session's state file whose pid is dead,
11
12
  * replay its still-open prompt (if any) into an `interrupted` record, then
@@ -46,9 +47,10 @@ export function recoverStaleSessions(input) {
46
47
  // `now` is only a fallback when no usable timestamp exists.
47
48
  const lastTs = last.events[last.events.length - 1]?.ts;
48
49
  const settledAt = typeof lastTs === "number" && Number.isFinite(lastTs) ? lastTs : input.now;
49
- const core = replayPrompt(last, { settledAt });
50
+ const costNow = readCost(input.env, sessionId);
51
+ const core = replayPrompt(last, { settledAt, cost: settleCost(costNow?.totalUsd, state.promptOpen.costAtStart).cost });
50
52
  if (core) {
51
- const model = readCost(input.env, sessionId)?.model;
53
+ const model = costNow?.model;
52
54
  records.push(buildClaudeRecord(core, state, sessionId, model, input.resolveAssignment?.(state.cwd, sessionId)));
53
55
  }
54
56
  }
package/dist/paths.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readdirSync } from "node:fs";
2
2
  import { isAbsolute, join } from "node:path";
3
3
  /**
4
- * Same rule as kankaku's `resolveKankakuDir` (kankaku/hub): an absolute
4
+ * Same rule as `resolveKankakuDir` in the `kankaku-pi` hub barrel (`kankaku-pi/hub`): an absolute
5
5
  * `KANKAKU_DIR` is used as-is, a relative one is joined against the session's
6
6
  * cwd. Inlined rather than imported so the light hooks (one per tool call)
7
7
  * never load the hub barrel for a two-line helper.
package/dist/record.js CHANGED
@@ -24,7 +24,7 @@ const PLUGIN_VERSION = readPackageVersion(join(dirname(dirname(fileURLToPath(imp
24
24
  * caller decides which cost snapshot's model applies.
25
25
  *
26
26
  * Also stamps who MEASURED the record (`agent`, `plugin`, `pluginVersion`),
27
- * so a worklog later synced by another tool (the kankaku TUI) keeps the
27
+ * so a worklog later synced by another tool (the `kankaku` CLI) keeps the
28
28
  * right identity. `agentVersion` is omitted: Claude Code passes its version
29
29
  * to no hook payload, and spawning `claude --version` from a hook is not
30
30
  * acceptable.
package/dist/replay.js CHANGED
@@ -1,11 +1,11 @@
1
- import { WorkTracker } from "kankaku/domain";
1
+ import { WorkTracker } from "kankaku-pi/domain";
2
2
  /** Tool names whose span counts as waiting rather than working. */
3
3
  const INTERACTIVE_TOOLS = ["AskUserQuestion"];
4
4
  /**
5
5
  * The plugin's own subagent profile: Claude Code's `Agent`/`Task` tool
6
6
  * calls open a subagent span, read from `subagent_type`. No join key is
7
- * offered (`SubagentStart`/`SubagentStop` are recorded but not linked — see
8
- * odd/tasks/hook-tracking.md "Replay (pure)").
7
+ * offered (`SubagentStart`/`SubagentStop` are recorded but not linked, so a
8
+ * subagent span is closed by the replay itself).
9
9
  */
10
10
  const CLAUDE_CODE_SUBAGENT_PROFILE = {
11
11
  id: "claude-code",
@@ -97,5 +97,10 @@ export function replayPrompt(prompt, opts = {}) {
97
97
  if (terminalStopIndex >= 0)
98
98
  return settled;
99
99
  current = opts.settledAt ?? prompt.events[prompt.events.length - 1].ts;
100
- return tracker.onShutdown();
100
+ const interrupted = tracker.onShutdown();
101
+ if (!interrupted || typeof opts.cost !== "number" || !Number.isFinite(opts.cost))
102
+ return interrupted;
103
+ // An interrupted prompt ends no turn, so the tracker saw no cost; stamp the
104
+ // measured delta without inventing a turn.
105
+ return { ...interrupted, usage: { ...interrupted.usage, cost: interrupted.usage.cost + opts.cost }, costObserved: true };
101
106
  }
package/dist/report.js CHANGED
@@ -1,4 +1,4 @@
1
- import { buildTasks } from "kankaku/domain";
1
+ import { buildTasks } from "kankaku-pi/domain";
2
2
  const STATUS_WORD = {
3
3
  completed: "done",
4
4
  interrupted: "interrupted",
package/dist/sync-cli.js CHANGED
@@ -2,9 +2,9 @@ import { readFileSync } from "node:fs";
2
2
  import { homedir, hostname, tmpdir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { CachedCatalog, JsonlWorkLog, PocketBaseClient, PocketBaseSink, SyncStateStore, computeSyncStatus, createPocketBaseCatalogFetcher, resolveHubCredentials, runSync, safeHomeDir, } from "kankaku/hub";
5
+ import { CachedCatalog, JsonlWorkLog, PocketBaseClient, PocketBaseSink, SyncStateStore, computeSyncStatus, createPocketBaseCatalogFetcher, resolveHubCredentials, runSync, safeHomeDir, } from "kankaku-pi/hub";
6
6
  import { resolveKankakuDir } from "./paths.js";
7
- const usage = "usage: node src/cli.ts sync [all|status]\n";
7
+ const usage = "usage: node dist/cli.js sync [all|status]\n";
8
8
  function windowHours(env) {
9
9
  const value = Number(env.KANKAKU_SYNC_WINDOW_HOURS);
10
10
  return env.KANKAKU_SYNC_WINDOW_HOURS && Number.isFinite(value) && value > 0 ? value : 24;
package/dist/task-cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
- import { CachedCatalog, PocketBaseClient, createPocketBaseCatalogFetcher, resolveHubCredentials } from "kankaku/hub";
3
+ import { CachedCatalog, PocketBaseClient, createPocketBaseCatalogFetcher, resolveHubCredentials } from "kankaku-pi/hub";
4
4
  import { findSession } from "./find-session.js";
5
5
  import { resolveKankakuDir, resolveTargetFile } from "./paths.js";
6
6
  import { readSessionTarget, writeSessionTarget } from "./session-target-store.js";
@@ -2,7 +2,7 @@
2
2
  * Pure task selection for `/kankaku:task`. Mirrors pi's picker
3
3
  * (`adapters/target-picker.ts#pickHubTask`): a task is pickable when its
4
4
  * status is not `done` (the hub's statuses are `open | doing | done`, see
5
- * `HubTask` in `kankaku/domain`) and it belongs to the resolved project.
5
+ * `HubTask` in `kankaku-pi/domain`) and it belongs to the resolved project.
6
6
  */
7
7
  export function isOpenTask(task) {
8
8
  return task.status !== "done";
@@ -1,7 +1,7 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
- import { CachedCatalog, readProjectClient, readProjectTargetIds, resolveHubCredentials, } from "kankaku/hub";
4
- import { formatWorkTargetLabel, isValidClient, resolveClient, resolveWorkTarget, resolveWorkTargetSource, } from "kankaku/domain";
3
+ import { CachedCatalog, readProjectClient, readProjectTargetIds, resolveHubCredentials, } from "kankaku-pi/hub";
4
+ import { formatWorkTargetLabel, isValidClient, resolveClient, resolveWorkTarget, resolveWorkTargetSource, } from "kankaku-pi/domain";
5
5
  import { findSession } from "./find-session.js";
6
6
  import { resolveKankakuDir, resolveTargetFile } from "./paths.js";
7
7
  import { readSessionTarget } from "./session-target-store.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kankaku-claude",
3
- "version": "0.12.1",
3
+ "version": "1.0.1",
4
4
  "description": "Claude Code plugin that records how long the agent works on each user prompt, in kankaku's worklog format.",
5
5
  "type": "module",
6
6
  "files": [
@@ -27,7 +27,7 @@
27
27
  "prepublishOnly": "npm run check"
28
28
  },
29
29
  "dependencies": {
30
- "kankaku": "^0.12.1"
30
+ "kankaku-pi": "^1.0.1"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^24.13.4",