kankaku-claude 0.1.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 +11 -0
- package/CHANGELOG.md +55 -0
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/commands/report.md +9 -0
- package/commands/setup.md +12 -0
- package/commands/status.md +9 -0
- package/commands/sync.md +14 -0
- package/hooks/hooks.json +103 -0
- package/package.json +34 -0
- package/src/claude-pid.ts +68 -0
- package/src/cli-core.ts +100 -0
- package/src/cli.ts +19 -0
- package/src/cost-store.ts +138 -0
- package/src/event-log.ts +33 -0
- package/src/events.ts +145 -0
- package/src/handle-hook.ts +275 -0
- package/src/hook.ts +41 -0
- package/src/inflight-recovery.ts +82 -0
- package/src/paths.ts +50 -0
- package/src/prompts.ts +42 -0
- package/src/record.ts +28 -0
- package/src/replay.ts +123 -0
- package/src/report.ts +132 -0
- package/src/session-state.ts +71 -0
- package/src/statusline-core.ts +89 -0
- package/src/statusline.ts +36 -0
- package/src/sync-cli.ts +99 -0
- package/tsconfig.json +14 -0
package/src/sync-cli.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { homedir, hostname, tmpdir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import {
|
|
6
|
+
CachedCatalog, JsonlWorkLog, PocketBaseClient, PocketBaseSink, SyncStateStore,
|
|
7
|
+
computeSyncStatus, createPocketBaseCatalogFetcher, resolveHubCredentials,
|
|
8
|
+
runSync, safeHomeDir,
|
|
9
|
+
} from "kankaku/hub";
|
|
10
|
+
import { resolveKankakuDir } from "./paths.ts";
|
|
11
|
+
import type { CliResult } from "./cli-core.ts";
|
|
12
|
+
|
|
13
|
+
export interface SyncCliDeps {
|
|
14
|
+
env: NodeJS.ProcessEnv;
|
|
15
|
+
cwd: string;
|
|
16
|
+
now: () => number;
|
|
17
|
+
homeDir?: () => string;
|
|
18
|
+
hostname?: () => string;
|
|
19
|
+
fetch?: typeof fetch;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const usage = "usage: node src/cli.ts sync [all|status]\n";
|
|
23
|
+
|
|
24
|
+
function windowHours(env: NodeJS.ProcessEnv): number {
|
|
25
|
+
const value = Number(env.KANKAKU_SYNC_WINDOW_HOURS);
|
|
26
|
+
return env.KANKAKU_SYNC_WINDOW_HOURS && Number.isFinite(value) && value > 0 ? value : 24;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function promptMode(env: NodeJS.ProcessEnv): "none" | "truncated" | "full" {
|
|
30
|
+
const value = env.KANKAKU_SYNC_PROMPT;
|
|
31
|
+
return value === "truncated" || value === "full" ? value : "none";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function packageVersion(): string {
|
|
35
|
+
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
36
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { version: string };
|
|
37
|
+
return pkg.version;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function runSyncCli(args: string[], deps: SyncCliDeps): Promise<CliResult> {
|
|
41
|
+
if (args.length > 1 || (args[0] !== undefined && args[0] !== "all" && args[0] !== "status")) {
|
|
42
|
+
return { stdout: "", stderr: usage, exitCode: 1 };
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const homeDir = deps.homeDir ?? (() => deps.env.HOME || homedir());
|
|
46
|
+
const hub = resolveHubCredentials({ env: deps.env, homeDir });
|
|
47
|
+
const dir = resolveKankakuDir(deps.env.KANKAKU_DIR ?? ".kankaku", deps.cwd);
|
|
48
|
+
const log = new JsonlWorkLog(dir);
|
|
49
|
+
const stateStore = new SyncStateStore({ dir, pid: process.pid, now: deps.now });
|
|
50
|
+
const target = hub.credentials?.url ?? stateStore.read()?.target ?? "";
|
|
51
|
+
const hours = windowHours(deps.env);
|
|
52
|
+
|
|
53
|
+
if (args[0] === "status") {
|
|
54
|
+
const { state, pending, staleOutsideWindow } = computeSyncStatus(log, stateStore, target, hours);
|
|
55
|
+
const lines = [
|
|
56
|
+
`hub: ${hub.credentials ? "configured" : "unconfigured"}`,
|
|
57
|
+
`pending: ${pending}`,
|
|
58
|
+
`staleOutsideWindow: ${staleOutsideWindow}`,
|
|
59
|
+
`syncedThrough: ${state?.syncedThrough ?? "none"}`,
|
|
60
|
+
];
|
|
61
|
+
if (state?.lastError) lines.push(`last error: ${state.lastError.message} (${state.lastError.at})`);
|
|
62
|
+
if (hub.invalidReason) lines.push(`hub URL: ${hub.invalidReason}`);
|
|
63
|
+
return { stdout: lines.join("\n") + "\n", exitCode: 0 };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (hub.invalidReason) return { stdout: "", stderr: `kankaku sync: invalid hub URL: ${hub.invalidReason}\n`, exitCode: 1 };
|
|
67
|
+
if (!hub.credentials) return { stdout: "", stderr: "kankaku sync: hub credentials are not configured (KANKAKU_PB_URL, KANKAKU_PB_EMAIL, KANKAKU_PB_PASSWORD).\n", exitCode: 1 };
|
|
68
|
+
|
|
69
|
+
const credentials = hub.credentials;
|
|
70
|
+
const client = new PocketBaseClient({ ...credentials, ...(deps.fetch ? { fetch: deps.fetch } : {}) });
|
|
71
|
+
const catalog = new CachedCatalog({
|
|
72
|
+
filePath: join(safeHomeDir(homeDir) ?? tmpdir(), ".kankaku", "catalog.json"),
|
|
73
|
+
url: credentials.url,
|
|
74
|
+
clock: { now: deps.now },
|
|
75
|
+
fetchCatalog: createPocketBaseCatalogFetcher(client),
|
|
76
|
+
});
|
|
77
|
+
// Refresh before resolving assignments; on an offline hub the cached snapshot remains usable.
|
|
78
|
+
await catalog.refresh();
|
|
79
|
+
const snapshot = catalog.read();
|
|
80
|
+
const sink = new PocketBaseSink({
|
|
81
|
+
client, clients: snapshot?.clients ?? [], projects: snapshot?.projects ?? [],
|
|
82
|
+
machine: deps.env.KANKAKU_MACHINE || (deps.hostname ?? hostname)(),
|
|
83
|
+
promptMode: promptMode(deps.env), syncRecords: deps.env.KANKAKU_SYNC_RECORDS !== "0",
|
|
84
|
+
agent: "claude-code", plugin: "kankaku-claude", pluginVersion: packageVersion(),
|
|
85
|
+
});
|
|
86
|
+
const summary = await runSync({
|
|
87
|
+
log, sink, stateStore, clock: { now: deps.now }, target: credentials.url, windowHours: hours,
|
|
88
|
+
}, { full: args[0] === "all" });
|
|
89
|
+
const stdout = `uploaded: ${summary.uploaded}, updated: ${summary.updated}, skipped: ${summary.skipped}, failed: ${summary.failed.length}\n`;
|
|
90
|
+
if (summary.locked) return { stdout, stderr: "kankaku sync: another sync is running.\n", exitCode: 1 };
|
|
91
|
+
if (summary.error || summary.failed.length) {
|
|
92
|
+
const reasons = [summary.error, ...summary.failed.map((f) => `${f.id}: ${f.reason}`)].filter(Boolean);
|
|
93
|
+
return { stdout, stderr: `kankaku sync: ${reasons.join("; ")}\n`, exitCode: 1 };
|
|
94
|
+
}
|
|
95
|
+
return { stdout, exitCode: 0 };
|
|
96
|
+
} catch (error) {
|
|
97
|
+
return { stdout: "", stderr: `kankaku sync: ${error instanceof Error ? error.message : String(error)}\n`, exitCode: 1 };
|
|
98
|
+
}
|
|
99
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "nodenext",
|
|
4
|
+
"moduleResolution": "nodenext",
|
|
5
|
+
"target": "es2022",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"allowImportingTsExtensions": true,
|
|
9
|
+
"erasableSyntaxOnly": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"verbatimModuleSyntax": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src", "tests"]
|
|
14
|
+
}
|