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 +7 -0
- package/README.md +21 -12
- package/commands/doctor.md +9 -0
- package/commands/sync-all.md +9 -0
- package/commands/sync-status.md +9 -0
- package/package.json +1 -1
- package/src/cli-core.ts +4 -1
- package/src/doctor.ts +89 -0
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
|
|
89
|
-
`node src/cli.ts sync`; the slash command does not forward arguments).
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
`node src/cli.ts
|
|
93
|
-
|
|
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
|
-
`
|
|
101
|
-
|
|
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`
|
|
185
|
-
|
|
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
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
|
+
}
|