kankaku 0.4.5 → 0.5.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.
Files changed (44) hide show
  1. package/README.md +964 -16
  2. package/package.json +4 -2
  3. package/src/adapters/agent-info.ts +86 -0
  4. package/src/adapters/ancestry.ts +260 -0
  5. package/src/adapters/cached-catalog.ts +131 -0
  6. package/src/adapters/file-inflight-store.ts +32 -0
  7. package/src/adapters/file-modes.ts +35 -0
  8. package/src/adapters/hub-credentials.ts +95 -0
  9. package/src/adapters/jsonl-work-log.ts +19 -2
  10. package/src/adapters/kankaku-command.ts +729 -0
  11. package/src/adapters/kankaku-dir.ts +92 -0
  12. package/src/adapters/lazy-jsonl-work-log.ts +4 -0
  13. package/src/adapters/machine-process-registry.ts +256 -0
  14. package/src/adapters/pi-tracker.ts +432 -266
  15. package/src/adapters/pocketbase-catalog.ts +60 -0
  16. package/src/adapters/pocketbase-client.ts +197 -0
  17. package/src/adapters/pocketbase-sink.ts +224 -0
  18. package/src/adapters/process-identity-memo.ts +102 -0
  19. package/src/adapters/process-identity.ts +162 -0
  20. package/src/adapters/project-config.ts +73 -1
  21. package/src/adapters/report.ts +89 -8
  22. package/src/adapters/session-client.ts +116 -0
  23. package/src/adapters/session-dir.ts +28 -0
  24. package/src/adapters/session-target.ts +262 -0
  25. package/src/adapters/status-bar.ts +86 -0
  26. package/src/adapters/subagent-startup.ts +66 -0
  27. package/src/adapters/sync-runner.ts +301 -0
  28. package/src/adapters/sync-state-store.ts +227 -0
  29. package/src/adapters/target-picker.ts +82 -0
  30. package/src/config.ts +444 -6
  31. package/src/domain/ancestry-match.ts +84 -0
  32. package/src/domain/hub-entry.ts +339 -0
  33. package/src/domain/registry-health.ts +87 -0
  34. package/src/domain/subagent-profile.ts +495 -0
  35. package/src/domain/sync-plan.ts +251 -0
  36. package/src/domain/task-view.ts +307 -23
  37. package/src/domain/work-record.ts +165 -4
  38. package/src/domain/work-target.ts +185 -0
  39. package/src/domain/work-tracker.ts +257 -56
  40. package/src/extension.ts +303 -6
  41. package/src/ports/catalog.ts +31 -0
  42. package/src/ports/process-registry.ts +75 -0
  43. package/src/ports/work-log.ts +9 -0
  44. package/src/ports/work-sink.ts +35 -0
@@ -0,0 +1,95 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { loadHubEnvCredentials, validateHubUrl } from "../config.ts";
4
+
5
+ const CREDENTIALS_FILE = join(".kankaku", "credentials.json");
6
+
7
+ export interface HubCredentials {
8
+ url: string;
9
+ email: string;
10
+ password: string;
11
+ }
12
+
13
+ interface HubCredentialsFile {
14
+ url?: string;
15
+ email?: string;
16
+ password?: string;
17
+ }
18
+
19
+ /**
20
+ * Read `<homeDir>/.kankaku/credentials.json`. Tolerates a missing file,
21
+ * malformed JSON, a non-object document, or non-string fields — all
22
+ * return `{}` rather than throwing, since this file is optional and
23
+ * hand-edited (mirrors `project-config.ts#readProjectClient`).
24
+ */
25
+ function readCredentialsFile(filePath: string): HubCredentialsFile {
26
+ if (!existsSync(filePath)) return {};
27
+
28
+ try {
29
+ const parsed: unknown = JSON.parse(readFileSync(filePath, "utf8"));
30
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
31
+ const record = parsed as Record<string, unknown>;
32
+ return {
33
+ url: typeof record["url"] === "string" ? record["url"] : undefined,
34
+ email: typeof record["email"] === "string" ? record["email"] : undefined,
35
+ password: typeof record["password"] === "string" ? record["password"] : undefined,
36
+ };
37
+ } catch {
38
+ return {};
39
+ }
40
+ }
41
+
42
+ export interface ResolveHubCredentialsDeps {
43
+ env: NodeJS.ProcessEnv;
44
+ /**
45
+ * Resolves the home directory; called lazily, inside this function, and
46
+ * defensively. A throwing provider (no `HOME`, a sandboxed environment
47
+ * without a resolvable home directory) is treated the same as "no home
48
+ * directory" rather than propagating — env-only credentials must still
49
+ * resolve, and the hub-unconfigured case must stay a no-op regardless of
50
+ * the host environment. Injectable for tests; defaults to `os.homedir` at
51
+ * the call site (extension.ts) — passed as a reference, never invoked
52
+ * there, so a throw never escapes before this function's own try/catch.
53
+ */
54
+ homeDir: () => string;
55
+ }
56
+
57
+ export interface ResolveHubCredentialsResult {
58
+ /** `undefined` when unconfigured (no url/email/password from any source) or the URL is refused. */
59
+ credentials: HubCredentials | undefined;
60
+ /** Set only when a hub URL was given but rejected by {@link validateHubUrl}; surface it once via `ctx.ui.notify`. */
61
+ invalidReason?: string;
62
+ }
63
+
64
+ /**
65
+ * Resolve hub credentials: `KANKAKU_PB_URL`/`_EMAIL`/`_PASSWORD` take
66
+ * precedence per field over `~/.kankaku/credentials.json` (so a partially
67
+ * set environment still combines with the file rather than failing
68
+ * closed). Never reads the project's own `.kankaku/config.json` — that
69
+ * file is project-local and frequently committed.
70
+ */
71
+ /** Resolve `homeDir()` defensively: any failure (no `HOME`, a sandboxed environment) yields `undefined` instead of throwing. Exported so callers with their own homedir-dependent path (e.g. `extension.ts`'s catalog cache) can share the same guard. */
72
+ export function safeHomeDir(homeDir: () => string): string | undefined {
73
+ try {
74
+ return homeDir();
75
+ } catch {
76
+ return undefined;
77
+ }
78
+ }
79
+
80
+ export function resolveHubCredentials(deps: ResolveHubCredentialsDeps): ResolveHubCredentialsResult {
81
+ const fromEnv = loadHubEnvCredentials(deps.env);
82
+ const homeDir = safeHomeDir(deps.homeDir);
83
+ const fromFile = homeDir !== undefined ? readCredentialsFile(join(homeDir, CREDENTIALS_FILE)) : {};
84
+
85
+ const url = fromEnv.url ?? fromFile.url;
86
+ const email = fromEnv.email ?? fromFile.email;
87
+ const password = fromEnv.password ?? fromFile.password;
88
+
89
+ if (!url || !email || !password) return { credentials: undefined };
90
+
91
+ const validation = validateHubUrl(url);
92
+ if (!validation.ok) return { credentials: undefined, invalidReason: validation.reason };
93
+
94
+ return { credentials: { url, email, password } };
95
+ }
@@ -1,5 +1,6 @@
1
- import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { dedupeById } from "../domain/task-view.ts";
3
4
  import type { WorkLog } from "../ports/work-log.ts";
4
5
  import type { WorkRecord } from "../domain/work-record.ts";
5
6
  import { isWorkRecord } from "../domain/work-record.ts";
@@ -27,6 +28,20 @@ export class JsonlWorkLog implements WorkLog {
27
28
  appendFileSync(this.filePath, `${JSON.stringify(record)}\n`);
28
29
  }
29
30
 
31
+ /**
32
+ * Cheap change signal: `mtimeMs:size` of the log file, computed with a
33
+ * single `statSync` rather than reading the file. `"0:0"` when the file
34
+ * does not exist yet (before the first `append`).
35
+ */
36
+ version(): string {
37
+ try {
38
+ const stats = statSync(this.filePath);
39
+ return `${stats.mtimeMs}:${stats.size}`;
40
+ } catch {
41
+ return "0:0";
42
+ }
43
+ }
44
+
30
45
  readAll(): WorkRecord[] {
31
46
  if (!existsSync(this.filePath)) return [];
32
47
  const content = readFileSync(this.filePath, "utf8");
@@ -45,6 +60,8 @@ export class JsonlWorkLog implements WorkLog {
45
60
  // Tolerate malformed lines (e.g. a torn write); skip them.
46
61
  }
47
62
  }
48
- return records;
63
+ // One id can be in the file twice (written at settle, then re-appended by
64
+ // crash recovery): every reader gets it once — see dedupeById.
65
+ return dedupeById(records);
49
66
  }
50
67
  }