kankaku-claude 0.2.0 → 0.2.2

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/CHANGELOG.md CHANGED
@@ -8,6 +8,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
8
8
 
9
9
  ### Added
10
10
 
11
+ - `/kankaku:doctor` and `node src/cli.ts doctor` provide a read-only local
12
+ diagnostic of plugin files, sessions, cost visibility and hub sync state,
13
+ without network requests or credential output.
14
+
15
+ - `/kankaku:sync-status` and `/kankaku:sync-all` slash commands for local
16
+ sync state inspection and full hub sync, respectively.
17
+
11
18
  - Phase 1: per-prompt work records from Claude Code hooks. Writes one
12
19
  `WorkRecord` per user prompt to `<KANKAKU_DIR>/worklog.jsonl`, in the same
13
20
  schema the kankaku pi extension writes, so kankaku's existing
package/README.md CHANGED
@@ -85,20 +85,29 @@ anything behind in whatever project happens to be open.
85
85
  and the last cost the statusline reported (wraps `node src/cli.ts status`).
86
86
  - `/kankaku:setup` — prints the `statusLine` snippet described above (wraps
87
87
  `node src/cli.ts setup`).
88
- - `/kankaku:sync` — manually syncs local work records to the hub (wraps
89
- `node src/cli.ts sync`; the slash command does not forward arguments).
90
-
91
- The report, status, and setup subcommands are also available directly via
92
- `node src/cli.ts <report|status|setup>`; `report` accepts `--days N` and
93
- defaults to the last 7 days.
88
+ - `/kankaku:sync` — manually syncs recent local work records to the hub
89
+ (wraps `node src/cli.ts sync`; the slash command does not forward arguments).
90
+ - `/kankaku:sync-status` — inspects local pending counts and sync state
91
+ (wraps `node src/cli.ts sync status`; no hub request or credentials required).
92
+ - `/kankaku:sync-all` — requests a full sync (wraps `node src/cli.ts sync all`).
93
+ - `/kankaku:doctor` — a read-only local diagnostic of plugin files, session and
94
+ cost visibility, and hub sync state (wraps `node src/cli.ts doctor`).
95
+
96
+ The report, status, setup, and doctor subcommands are also available directly via
97
+ `node src/cli.ts <report|status|setup|doctor>`; `report` accepts `--days N` and
98
+ defaults to the last 7 days. Doctor reads only local data; it never contacts the
99
+ hub or prints credentials. A missing cost file means cost has not been observed
100
+ under the current `HOME` (it does not prove the statusline is misconfigured).
101
+ Stored sync error details are withheld because they may contain sensitive data.
94
102
 
95
103
  ### Manual hub sync
96
104
 
97
105
  Configure hub credentials with `KANKAKU_PB_URL`, `KANKAKU_PB_EMAIL`, and
98
106
  `KANKAKU_PB_PASSWORD`, or use `~/.kankaku/credentials.json` (the shared
99
- kankaku hub credential file). Then run `/kankaku:sync` or
100
- `node src/cli.ts sync` in the project whose worklog you want to upload.
101
- For options not forwarded by the slash command, use the CLI directly:
107
+ kankaku hub credential file). Then run `/kankaku:sync` for recent records or
108
+ `/kankaku:sync-all` for a full sync in the project whose worklog you want to
109
+ upload. Use `/kankaku:sync-status` to inspect local sync state without a hub
110
+ request or credentials. The same operations are available directly via CLI:
102
111
 
103
112
  | Command | Purpose |
104
113
  |---------|---------|
@@ -181,9 +190,9 @@ also runs for the current session's own leftover state at `SessionEnd`.
181
190
  document a link between a `SubagentStart`/`SubagentStop` pair and the
182
191
  `tool_use_id` that launched it, so kankaku-claude cannot join them; the
183
192
  subagent's own time is not separately measured here.
184
- - **Hub sync is best-effort.** Use `/kankaku:sync` or the direct CLI to retry
185
- failures or request a full sync. Prompts are omitted from uploads unless
186
- `KANKAKU_SYNC_PROMPT` is `truncated` or `full`.
193
+ - **Hub sync is best-effort.** Use `/kankaku:sync` to retry recent records or
194
+ `/kankaku:sync-all` to request a full sync. Prompts are omitted from uploads
195
+ unless `KANKAKU_SYNC_PROMPT` is `truncated` or `full`.
187
196
 
188
197
  See [kankaku.io](https://kankaku.io) for the pi extension this plugin shares
189
198
  its worklog format with.
@@ -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
@@ -0,0 +1,9 @@
1
+ ---
2
+ description: Request a full sync of local kankaku work records to the hub
3
+ allowed-tools: Bash(node:*)
4
+ ---
5
+
6
+ Run the kankaku CLI sync all 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" sync all
@@ -0,0 +1,9 @@
1
+ ---
2
+ description: Show local kankaku hub sync status
3
+ allowed-tools: Bash(node:*)
4
+ ---
5
+
6
+ Run the kankaku CLI sync status 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" sync status
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kankaku-claude",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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": [
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
+ }