kankaku-claude 0.2.1 → 0.9.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 +2 -2
- package/CHANGELOG.md +13 -0
- package/README.md +12 -4
- package/commands/doctor.md +9 -0
- package/package.json +4 -3
- package/src/cli-core.ts +4 -1
- package/src/doctor.ts +89 -0
- package/src/sync-cli.ts +1 -1
|
@@ -1,11 +1,11 @@
|
|
|
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.9.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "soyunninja"
|
|
7
7
|
},
|
|
8
8
|
"homepage": "https://kankaku.io",
|
|
9
|
-
"repository": "https://github.com/soyunninja/kankaku
|
|
9
|
+
"repository": "https://github.com/soyunninja/kankaku",
|
|
10
10
|
"license": "MIT"
|
|
11
11
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -4,10 +4,23 @@ 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.9.0 — 2026-09-28
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Moved into the kankaku monorepo (`github.com/soyunninja/kankaku`,
|
|
12
|
+
`packages/claude`); versions are now lockstep with the other client
|
|
13
|
+
packages (`kankaku`, `kankaku-tui`). Depends on `kankaku ^0.9.0` (was
|
|
14
|
+
`^0.6.0`). No behaviour change.
|
|
15
|
+
|
|
7
16
|
## [Unreleased]
|
|
8
17
|
|
|
9
18
|
### Added
|
|
10
19
|
|
|
20
|
+
- `/kankaku:doctor` and `node src/cli.ts doctor` provide a read-only local
|
|
21
|
+
diagnostic of plugin files, sessions, cost visibility and hub sync state,
|
|
22
|
+
without network requests or credential output.
|
|
23
|
+
|
|
11
24
|
- `/kankaku:sync-status` and `/kankaku:sync-all` slash commands for local
|
|
12
25
|
sync state inspection and full hub sync, respectively.
|
|
13
26
|
|
package/README.md
CHANGED
|
@@ -4,6 +4,9 @@ A [Claude Code](https://claude.com/claude-code) plugin that records how long
|
|
|
4
4
|
Claude Code works on each of your prompts, in the same worklog format as the
|
|
5
5
|
[kankaku](https://kankaku.io) pi extension.
|
|
6
6
|
|
|
7
|
+
This package lives in the [kankaku monorepo](https://github.com/soyunninja/kankaku),
|
|
8
|
+
under `packages/claude`.
|
|
9
|
+
|
|
7
10
|
## What it measures
|
|
8
11
|
|
|
9
12
|
One record per user prompt, appended to `<KANKAKU_DIR>/worklog.jsonl`:
|
|
@@ -90,10 +93,15 @@ anything behind in whatever project happens to be open.
|
|
|
90
93
|
- `/kankaku:sync-status` — inspects local pending counts and sync state
|
|
91
94
|
(wraps `node src/cli.ts sync status`; no hub request or credentials required).
|
|
92
95
|
- `/kankaku:sync-all` — requests a full sync (wraps `node src/cli.ts sync all`).
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
- `/kankaku:doctor` — a read-only local diagnostic of plugin files, session and
|
|
97
|
+
cost visibility, and hub sync state (wraps `node src/cli.ts doctor`).
|
|
98
|
+
|
|
99
|
+
The report, status, setup, and doctor subcommands are also available directly via
|
|
100
|
+
`node src/cli.ts <report|status|setup|doctor>`; `report` accepts `--days N` and
|
|
101
|
+
defaults to the last 7 days. Doctor reads only local data; it never contacts the
|
|
102
|
+
hub or prints credentials. A missing cost file means cost has not been observed
|
|
103
|
+
under the current `HOME` (it does not prove the statusline is misconfigured).
|
|
104
|
+
Stored sync error details are withheld because they may contain sensitive data.
|
|
97
105
|
|
|
98
106
|
### Manual hub sync
|
|
99
107
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Show a read-only local kankaku installation and sync diagnostic
|
|
3
|
+
allowed-tools: Bash(node:*)
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Run the kankaku CLI doctor command and show its output to the user verbatim,
|
|
7
|
+
inside a code block, with no summarizing or reformatting:
|
|
8
|
+
|
|
9
|
+
!node "${CLAUDE_PLUGIN_ROOT}/src/cli.ts" doctor
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kankaku-claude",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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": [
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"author": "soyunninja",
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|
|
20
|
-
"url": "git+https://github.com/soyunninja/kankaku
|
|
20
|
+
"url": "git+https://github.com/soyunninja/kankaku.git",
|
|
21
|
+
"directory": "packages/claude"
|
|
21
22
|
},
|
|
22
23
|
"scripts": {
|
|
23
24
|
"test": "node --test tests/*.test.ts",
|
|
@@ -25,7 +26,7 @@
|
|
|
25
26
|
"check": "npm run typecheck && npm test"
|
|
26
27
|
},
|
|
27
28
|
"dependencies": {
|
|
28
|
-
"kankaku": "^0.
|
|
29
|
+
"kankaku": "^0.9.0"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
32
|
"@types/node": "^24.13.4",
|
package/src/cli-core.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { readState } from "./session-state.ts";
|
|
|
5
5
|
import { readCost } from "./cost-store.ts";
|
|
6
6
|
import { formatReport } from "./report.ts";
|
|
7
7
|
import { runSyncCli } from "./sync-cli.ts";
|
|
8
|
+
import { runDoctor } from "./doctor.ts";
|
|
8
9
|
|
|
9
10
|
export interface CliDeps {
|
|
10
11
|
env: NodeJS.ProcessEnv;
|
|
@@ -22,7 +23,7 @@ export interface CliResult {
|
|
|
22
23
|
stderr?: string;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
const USAGE = "usage: node src/cli.ts <report|status|setup|sync> [--days N]\n";
|
|
26
|
+
const USAGE = "usage: node src/cli.ts <report|status|setup|sync|doctor> [--days N]\n";
|
|
26
27
|
|
|
27
28
|
/** CLI commands, resolved from `deps.cwd`. */
|
|
28
29
|
export async function runCli(argv: string[], deps: CliDeps): Promise<CliResult> {
|
|
@@ -36,6 +37,8 @@ export async function runCli(argv: string[], deps: CliDeps): Promise<CliResult>
|
|
|
36
37
|
return runSetup(deps);
|
|
37
38
|
case "sync":
|
|
38
39
|
return runSyncCli(rest, deps);
|
|
40
|
+
case "doctor":
|
|
41
|
+
return { stdout: runDoctor(deps), exitCode: 0 };
|
|
39
42
|
default:
|
|
40
43
|
return { stdout: "", exitCode: 1, stderr: USAGE };
|
|
41
44
|
}
|
package/src/doctor.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { JsonlWorkLog, SyncStateStore, computeSyncStatus, resolveHubCredentials } from "kankaku/hub";
|
|
5
|
+
import type { CliDeps } from "./cli-core.ts";
|
|
6
|
+
import { costDir } from "./cost-store.ts";
|
|
7
|
+
import { listStateFiles, resolveKankakuDir } from "./paths.ts";
|
|
8
|
+
import { readState } from "./session-state.ts";
|
|
9
|
+
|
|
10
|
+
function metadata(file: string): Record<string, unknown> | undefined {
|
|
11
|
+
try {
|
|
12
|
+
const value: unknown = JSON.parse(readFileSync(file, "utf8"));
|
|
13
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
14
|
+
} catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function label(value: unknown): string {
|
|
20
|
+
// Metadata is local but not trusted: never echo arbitrary object or secret-looking fields.
|
|
21
|
+
return typeof value === "string" && /^[\w.-]+$/.test(value) ? value : "unavailable";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function syncWindowHours(env: NodeJS.ProcessEnv): number {
|
|
25
|
+
const hours = Number(env.KANKAKU_SYNC_WINDOW_HOURS);
|
|
26
|
+
return env.KANKAKU_SYNC_WINDOW_HOURS && Number.isFinite(hours) && hours > 0 ? hours : 24;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Read-only, local diagnostic. No sync runner or network client is reachable from this path. */
|
|
30
|
+
export function runDoctor(deps: CliDeps): string {
|
|
31
|
+
const dir = resolveKankakuDir(deps.env.KANKAKU_DIR ?? ".kankaku", deps.cwd);
|
|
32
|
+
const pkg = metadata(join(deps.pluginRoot, "package.json"));
|
|
33
|
+
const plugin = metadata(join(deps.pluginRoot, ".claude-plugin", "plugin.json"));
|
|
34
|
+
const worklogPath = join(dir, "worklog.jsonl");
|
|
35
|
+
const log = new JsonlWorkLog(dir);
|
|
36
|
+
let recordCount: string;
|
|
37
|
+
try { recordCount = String(log.readAll().length); } catch { recordCount = "unreadable"; }
|
|
38
|
+
const states = listStateFiles(join(dir, "claude"));
|
|
39
|
+
let alive = 0;
|
|
40
|
+
let dead = 0;
|
|
41
|
+
let open = 0;
|
|
42
|
+
let unreadable = 0;
|
|
43
|
+
for (const file of states) {
|
|
44
|
+
const state = readState(file);
|
|
45
|
+
if (!state) { unreadable++; continue; }
|
|
46
|
+
if (state.pid > 0 && Number.isInteger(state.pid) && deps.isAlive(state.pid)) alive++;
|
|
47
|
+
else dead++;
|
|
48
|
+
if (state.promptOpen) open++;
|
|
49
|
+
}
|
|
50
|
+
let costsVisible = false;
|
|
51
|
+
try { costsVisible = readdirSync(costDir(deps.env)).some((file) => file.endsWith(".json")); } catch { /* missing or inaccessible */ }
|
|
52
|
+
|
|
53
|
+
const hub = resolveHubCredentials({ env: deps.env, homeDir: () => deps.env.HOME || homedir() });
|
|
54
|
+
const store = new SyncStateStore({ dir, pid: process.pid, now: deps.now });
|
|
55
|
+
const target = hub.credentials?.url ?? store.read()?.target ?? "";
|
|
56
|
+
const { state, pending, staleOutsideWindow } = computeSyncStatus(log, store, target, syncWindowHours(deps.env));
|
|
57
|
+
const hubState = hub.invalidReason ? "invalid URL" : hub.credentials ? "configured" : "unconfigured";
|
|
58
|
+
const commands = ["report", "status", "setup", "sync", "sync-status", "sync-all", "doctor"];
|
|
59
|
+
const present = commands.filter((name) => existsSync(join(deps.pluginRoot, "commands", `${name}.md`))).length;
|
|
60
|
+
const actions = ["/kankaku:status", "/kankaku:sync-status"];
|
|
61
|
+
if (!costsVisible) actions.unshift("/kankaku:setup (configure the statusline for cost)");
|
|
62
|
+
if (hubState === "invalid URL") actions.push("Check KANKAKU_PB_URL (HTTPS required)");
|
|
63
|
+
else if (hubState === "unconfigured") actions.push("Configure hub credentials to enable sync");
|
|
64
|
+
|
|
65
|
+
return [
|
|
66
|
+
"# kankaku doctor", "", "## Package / plugin",
|
|
67
|
+
`package version: ${label(pkg?.version)}`,
|
|
68
|
+
`plugin name: ${label(plugin?.name)}`,
|
|
69
|
+
`plugin version: ${label(plugin?.version)}`,
|
|
70
|
+
`pluginRoot: ${deps.pluginRoot}`,
|
|
71
|
+
"", "## Local files",
|
|
72
|
+
`KANKAKU_DIR: ${dir}`,
|
|
73
|
+
`worklog: ${existsSync(worklogPath) ? "present" : "absent"} (${recordCount} records)`,
|
|
74
|
+
`command files present: ${present}/${commands.length}`,
|
|
75
|
+
`hooks file present: ${existsSync(join(deps.pluginRoot, "hooks", "hooks.json")) ? "yes" : "no"}`,
|
|
76
|
+
"", "## Sessions / cost",
|
|
77
|
+
`active sessions: ${states.length}`,
|
|
78
|
+
`alive: ${alive}; dead: ${dead}; open prompts: ${open}; unreadable: ${unreadable}`,
|
|
79
|
+
`cost files visible: ${costsVisible ? "yes" : "no"} (current HOME)`,
|
|
80
|
+
"", "## Hub / sync",
|
|
81
|
+
`hub: ${hubState}`,
|
|
82
|
+
`pending: ${pending}`,
|
|
83
|
+
`staleOutsideWindow: ${staleOutsideWindow}`,
|
|
84
|
+
`syncedThrough: ${typeof state?.syncedThrough === "string" && /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(state.syncedThrough) ? state.syncedThrough : "none"}`,
|
|
85
|
+
// Stored error messages can contain server responses or credentials; only show presence.
|
|
86
|
+
`last sync error: ${state?.lastError ? "present" : "none"}`,
|
|
87
|
+
"", "## Next actions", ...actions.map((action) => `- ${action}`), "",
|
|
88
|
+
].join("\n");
|
|
89
|
+
}
|
package/src/sync-cli.ts
CHANGED
|
@@ -106,7 +106,7 @@ export async function syncConfigured(
|
|
|
106
106
|
await catalog.refresh();
|
|
107
107
|
const snapshot = catalog.read();
|
|
108
108
|
return new PocketBaseSink({
|
|
109
|
-
client, clients: snapshot?.clients ?? [], projects: snapshot?.projects ?? [],
|
|
109
|
+
client, clients: snapshot?.clients ?? [], projects: snapshot?.projects ?? [], tasks: snapshot?.tasks ?? [],
|
|
110
110
|
machine: deps.env.KANKAKU_MACHINE || (deps.hostname ?? hostname)(),
|
|
111
111
|
promptMode: promptMode(deps.env), syncRecords: deps.env.KANKAKU_SYNC_RECORDS !== "0",
|
|
112
112
|
agent: "claude-code", plugin: "kankaku-claude", pluginVersion: packageVersion(),
|