kankaku-claude 0.2.1 → 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,10 @@ 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
+
11
15
  - `/kankaku:sync-status` and `/kankaku:sync-all` slash commands for local
12
16
  sync state inspection and full hub sync, respectively.
13
17
 
package/README.md CHANGED
@@ -90,10 +90,15 @@ anything behind in whatever project happens to be open.
90
90
  - `/kankaku:sync-status` — inspects local pending counts and sync state
91
91
  (wraps `node src/cli.ts sync status`; no hub request or credentials required).
92
92
  - `/kankaku:sync-all` — requests a full sync (wraps `node src/cli.ts sync all`).
93
-
94
- The report, status, and setup subcommands are also available directly via
95
- `node src/cli.ts <report|status|setup>`; `report` accepts `--days N` and
96
- defaults to the last 7 days.
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.
97
102
 
98
103
  ### Manual hub sync
99
104
 
@@ -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.2.1",
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
+ }