@sma1lboy/kobe 0.7.9 → 0.7.11

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 (2) hide show
  1. package/dist/cli/index.js +193 -112
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -70,7 +70,7 @@ var init_package = __esm(() => {
70
70
  package_default = {
71
71
  $schema: "https://json.schemastore.org/package.json",
72
72
  name: "@sma1lboy/kobe",
73
- version: "0.7.9",
73
+ version: "0.7.11",
74
74
  description: "TUI orchestrator for Claude Code (codename)",
75
75
  type: "module",
76
76
  packageManager: "bun@1.3.13",
@@ -5019,6 +5019,50 @@ function matchTaskByCwd(tasks, cwd) {
5019
5019
  }
5020
5020
  return bestId;
5021
5021
  }
5022
+ function matchRepoByCwd(tasks, cwd) {
5023
+ const target = normalize(cwd);
5024
+ let best;
5025
+ let bestLen = -1;
5026
+ for (const t of tasks) {
5027
+ if (!t.repo)
5028
+ continue;
5029
+ const repo = normalize(t.repo);
5030
+ if (isAncestorOrSelf(repo, target) && repo.length > bestLen) {
5031
+ bestLen = repo.length;
5032
+ best = repo;
5033
+ }
5034
+ }
5035
+ return best;
5036
+ }
5037
+ function findAdoptableWorktree(tasks, cwd) {
5038
+ const target = normalize(cwd);
5039
+ const repos = new Set;
5040
+ const known = new Set;
5041
+ for (const t of tasks) {
5042
+ if (t.repo)
5043
+ repos.add(normalize(t.repo));
5044
+ if (t.worktreePath)
5045
+ known.add(normalize(t.worktreePath));
5046
+ }
5047
+ const seg = `/${KOBE_WORKTREE_ROOT_SUBPATH}/`;
5048
+ for (const repo of repos) {
5049
+ const prefix = `${repo}${seg}`;
5050
+ if (!target.startsWith(prefix))
5051
+ continue;
5052
+ const rest = target.slice(prefix.length);
5053
+ const name = rest.split("/")[0];
5054
+ if (!name)
5055
+ continue;
5056
+ const worktreePath = `${prefix}${name}`;
5057
+ if (known.has(worktreePath))
5058
+ return;
5059
+ return { repo, worktreePath };
5060
+ }
5061
+ return;
5062
+ }
5063
+ var init_cwd_task = __esm(() => {
5064
+ init_paths();
5065
+ });
5022
5066
 
5023
5067
  // src/daemon/event-bus.ts
5024
5068
  class DaemonEventBus {
@@ -5335,6 +5379,20 @@ async function startDaemonServer(orch, options = {}) {
5335
5379
  });
5336
5380
  return { task: serializeTask(task) };
5337
5381
  }
5382
+ case "worktree.reconcile": {
5383
+ const cwd = requireString(payload, "cwd");
5384
+ const worktreePath = requireString(payload, "worktreePath");
5385
+ const repo = matchRepoByCwd(orch.listTasks(), cwd) ?? matchRepoByCwd(orch.listTasks(), worktreePath);
5386
+ if (!repo)
5387
+ return { adopted: false };
5388
+ try {
5389
+ const task = await orch.adoptWorktree({ repo, worktreePath, ifExists: "return" });
5390
+ return { adopted: true, taskId: task.id };
5391
+ } catch (err) {
5392
+ logDaemonError("worktree-created", err);
5393
+ return { adopted: false };
5394
+ }
5395
+ }
5338
5396
  case "task.setActive": {
5339
5397
  bus.publish("active-task", { taskId: optionalString(payload, "taskId") ?? null });
5340
5398
  return {};
@@ -5345,6 +5403,16 @@ async function startDaemonServer(orch, options = {}) {
5345
5403
  throw new Error(`unknown engine event kind: ${kind}`);
5346
5404
  const explicitId = optionalString(payload, "taskId");
5347
5405
  const cwd = optionalString(payload, "cwd");
5406
+ if (!explicitId && cwd && kind === "session-start") {
5407
+ const cand = findAdoptableWorktree(orch.listTasks(), cwd);
5408
+ if (cand) {
5409
+ try {
5410
+ await orch.adoptWorktree({ repo: cand.repo, worktreePath: cand.worktreePath, ifExists: "return" });
5411
+ } catch (err) {
5412
+ logDaemonError("worktree-autosync", err);
5413
+ }
5414
+ }
5415
+ }
5348
5416
  const taskId = explicitId ?? (cwd ? matchTaskByCwd(orch.listTasks(), cwd) : undefined);
5349
5417
  if (!taskId)
5350
5418
  return {};
@@ -5491,6 +5559,7 @@ var init_server = __esm(() => {
5491
5559
  init_hook_events();
5492
5560
  init_version();
5493
5561
  init_auto_title_poller();
5562
+ init_cwd_task();
5494
5563
  init_paths2();
5495
5564
  init_protocol();
5496
5565
  DEFAULT_UPDATE_POLL_MS = 6 * 60 * 60 * 1000;
@@ -8365,6 +8434,29 @@ function mergeActivityHooks(current, install, inv = kobeCliInvocation()) {
8365
8434
  }
8366
8435
  return Object.keys(hooks).length > 0 ? { ...restSettings, hooks } : { ...restSettings };
8367
8436
  }
8437
+ function buildWorktreeWatchHook(inv = kobeCliInvocation()) {
8438
+ const command = shellQuoteArgv([...inv, "hook", WORKTREE_SYNC_MARKER]);
8439
+ return { PostToolUse: [{ matcher: "Bash", hooks: [{ type: "command", command }] }] };
8440
+ }
8441
+ function isKobeWorktreeWatchGroup(group) {
8442
+ if (!isObject6(group) || !Array.isArray(group.hooks))
8443
+ return false;
8444
+ return group.hooks.some((h) => isObject6(h) && typeof h.command === "string" && h.command.includes(WORKTREE_SYNC_MARKER));
8445
+ }
8446
+ function mergeWorktreeWatchHook(current, install, inv = kobeCliInvocation()) {
8447
+ const { hooks: rawHooks, ...restSettings } = current;
8448
+ const hooks = isObject6(rawHooks) ? { ...rawHooks } : {};
8449
+ const key = "PostToolUse";
8450
+ const prior = Array.isArray(hooks[key]) ? hooks[key] : [];
8451
+ const kept = prior.filter((g) => !isKobeWorktreeWatchGroup(g));
8452
+ if (install)
8453
+ kept.push(...buildWorktreeWatchHook(inv)[key]);
8454
+ if (kept.length > 0)
8455
+ hooks[key] = kept;
8456
+ else
8457
+ delete hooks[key];
8458
+ return Object.keys(hooks).length > 0 ? { ...restSettings, hooks } : { ...restSettings };
8459
+ }
8368
8460
 
8369
8461
  class ClaudeHookAdapter {
8370
8462
  vendor = "claude";
@@ -8380,13 +8472,15 @@ class ClaudeHookAdapter {
8380
8472
  async removeActivityHooks(settingsFilePath) {
8381
8473
  await this.editSettings(settingsFilePath, (cur) => mergeActivityHooks(cur, false));
8382
8474
  }
8383
- async installWorktreeSyncHook(settingsFilePath) {
8384
- const command = shellQuoteArgv([...kobeCliInvocation(), "hook", "worktree-created"]);
8385
- await this.editSettings(settingsFilePath, (cur) => mergeWorktreeSyncHook(cur, command));
8386
- }
8387
8475
  async removeWorktreeSyncHook(settingsFilePath) {
8388
8476
  await this.editSettings(settingsFilePath, (cur) => mergeWorktreeSyncHook(cur, null));
8389
8477
  }
8478
+ async installWorktreeWatchHook(settingsFilePath) {
8479
+ await this.editSettings(settingsFilePath, (cur) => mergeWorktreeWatchHook(cur, true));
8480
+ }
8481
+ async removeWorktreeWatchHook(settingsFilePath) {
8482
+ await this.editSettings(settingsFilePath, (cur) => mergeWorktreeWatchHook(cur, false));
8483
+ }
8390
8484
  async editSettings(settingsFilePath, transform) {
8391
8485
  try {
8392
8486
  const current = await readJsonObject(settingsFilePath);
@@ -8434,8 +8528,9 @@ class NoopHookAdapter {
8434
8528
  supportsWorktreeSync() {
8435
8529
  return false;
8436
8530
  }
8437
- async installWorktreeSyncHook() {}
8438
8531
  async removeWorktreeSyncHook() {}
8532
+ async installWorktreeWatchHook() {}
8533
+ async removeWorktreeWatchHook() {}
8439
8534
  }
8440
8535
  var init_hook_adapter2 = __esm(() => {
8441
8536
  init_hook_adapter();
@@ -8445,10 +8540,11 @@ var init_hook_adapter2 = __esm(() => {
8445
8540
  var exports_hook_cmd = {};
8446
8541
  __export(exports_hook_cmd, {
8447
8542
  runHookSubcommand: () => runHookSubcommand,
8543
+ parseWorktreeAddPath: () => parseWorktreeAddPath,
8448
8544
  ensureGlobalKobeHooks: () => ensureGlobalKobeHooks
8449
8545
  });
8450
8546
  import { homedir as homedir10 } from "os";
8451
- import { dirname as dirname7, join as join9, resolve as resolve6 } from "path";
8547
+ import { join as join9, resolve as resolve6 } from "path";
8452
8548
  async function readStdinPayload() {
8453
8549
  try {
8454
8550
  const text = await Promise.race([
@@ -8481,17 +8577,70 @@ function flagValue(argv, name) {
8481
8577
  }
8482
8578
  return;
8483
8579
  }
8580
+ function isPlainObject2(v) {
8581
+ return !!v && typeof v === "object" && !Array.isArray(v);
8582
+ }
8583
+ function tokenizeCommand(command) {
8584
+ const out = [];
8585
+ const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
8586
+ let m;
8587
+ while ((m = re.exec(command)) !== null)
8588
+ out.push(m[1] ?? m[2] ?? m[3] ?? "");
8589
+ return out;
8590
+ }
8591
+ function parseWorktreeAddPath(command) {
8592
+ const tokens = tokenizeCommand(command);
8593
+ const valueFlags = new Set(["-b", "-B", "--reason"]);
8594
+ for (let i = 0;i + 1 < tokens.length; i++) {
8595
+ if (tokens[i] !== "worktree" || tokens[i + 1] !== "add")
8596
+ continue;
8597
+ let j = i + 2;
8598
+ while (j < tokens.length) {
8599
+ const t = tokens[j];
8600
+ if (t === "&&" || t === "||" || t === ";" || t === "|" || t === ">" || t === ">>")
8601
+ break;
8602
+ if (t.startsWith("-")) {
8603
+ j += valueFlags.has(t) ? 2 : 1;
8604
+ continue;
8605
+ }
8606
+ return t;
8607
+ }
8608
+ }
8609
+ return;
8610
+ }
8611
+ async function runWorktreeCreatedHook() {
8612
+ const payload = await readStdinPayload();
8613
+ const toolInput = isPlainObject2(payload.tool_input) ? payload.tool_input : {};
8614
+ const command = typeof toolInput.command === "string" ? toolInput.command : "";
8615
+ if (!command.includes("worktree"))
8616
+ return;
8617
+ const rawPath = parseWorktreeAddPath(command);
8618
+ if (!rawPath)
8619
+ return;
8620
+ const cwd = typeof payload.cwd === "string" && payload.cwd ? payload.cwd : process.cwd();
8621
+ const worktreePath = resolve6(cwd, rawPath);
8622
+ const client = await connectIfRunning();
8623
+ if (!client)
8624
+ return;
8625
+ try {
8626
+ await client.request("worktree.reconcile", { cwd, worktreePath });
8627
+ } finally {
8628
+ client.close();
8629
+ }
8630
+ }
8484
8631
  async function runHookSubcommand(argv) {
8485
8632
  const [verb, ...rest] = argv;
8486
8633
  if (verb === "setup") {
8487
8634
  await runHookSetup(rest);
8488
8635
  return;
8489
8636
  }
8637
+ if (verb === WORKTREE_CREATED_VERB) {
8638
+ try {
8639
+ await runWorktreeCreatedHook();
8640
+ } catch {}
8641
+ return;
8642
+ }
8490
8643
  try {
8491
- if (verb === "worktree-created") {
8492
- await reportWorktreeCreated();
8493
- return;
8494
- }
8495
8644
  if (!verb || !isEngineActivityKind(verb))
8496
8645
  return;
8497
8646
  const payload = await readStdinPayload();
@@ -8517,43 +8666,6 @@ async function runHookSubcommand(argv) {
8517
8666
  }
8518
8667
  } catch {}
8519
8668
  }
8520
- async function reportWorktreeCreated() {
8521
- const payload = await readStdinPayload();
8522
- const worktreePath = typeof payload.worktree_path === "string" ? payload.worktree_path : undefined;
8523
- if (!worktreePath)
8524
- return;
8525
- const repo = await deriveRepoRoot(worktreePath);
8526
- if (!repo)
8527
- return;
8528
- const client = await connectIfRunning();
8529
- if (!client)
8530
- return;
8531
- try {
8532
- await client.request("worktree.adopt", { repo, worktreePath, ifExists: "return" });
8533
- } finally {
8534
- client.close();
8535
- }
8536
- }
8537
- async function deriveRepoRoot(worktreePath) {
8538
- try {
8539
- const proc = Bun.spawn(["git", "-C", worktreePath, "rev-parse", "--path-format=absolute", "--git-common-dir"], {
8540
- stdout: "pipe",
8541
- stderr: "ignore"
8542
- });
8543
- const [out, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
8544
- if (code !== 0)
8545
- return;
8546
- const commonDir = out.trim();
8547
- return commonDir ? dirname7(commonDir) : undefined;
8548
- } catch {
8549
- return;
8550
- }
8551
- }
8552
- function syncSettingsPath(scope) {
8553
- if (scope.kind === "repo")
8554
- return join9(resolve6(scope.path), ".claude", "settings.json");
8555
- return globalSettingsPath();
8556
- }
8557
8669
  function worktreeSyncAdapters() {
8558
8670
  return ALL_VENDORS.map((v) => createEngineHookAdapter(v)).filter((a) => a.supportsWorktreeSync());
8559
8671
  }
@@ -8567,84 +8679,53 @@ function persistedSyncPath(stored) {
8567
8679
  if (!stored || stored === "off")
8568
8680
  return;
8569
8681
  if (stored === "global")
8570
- return syncSettingsPath({ kind: "global" });
8682
+ return globalSettingsPath();
8571
8683
  if (stored.startsWith("repo:"))
8572
- return syncSettingsPath({ kind: "repo", path: stored.slice(5) });
8684
+ return join9(resolve6(stored.slice(5)), ".claude", "settings.json");
8573
8685
  return stored;
8574
8686
  }
8575
8687
  async function ensureGlobalKobeHooks() {
8576
8688
  try {
8577
8689
  const globalPath = globalSettingsPath();
8578
- for (const a of activityHookAdapters())
8690
+ for (const a of activityHookAdapters()) {
8579
8691
  await a.installActivityHooks(globalPath);
8580
- const stored = getPersistedString(SYNC_SETTING_KEY);
8581
- if (stored === "off")
8582
- return;
8583
- const syncAdapters = worktreeSyncAdapters();
8584
- if (syncAdapters.length === 0)
8585
- return;
8586
- const path6 = persistedSyncPath(stored) ?? syncSettingsPath({ kind: "global" });
8587
- for (const a of syncAdapters)
8588
- await a.installWorktreeSyncHook(path6);
8589
- if (!stored)
8590
- setPersistedString(SYNC_SETTING_KEY, path6);
8692
+ await a.installWorktreeWatchHook(globalPath);
8693
+ }
8694
+ await cleanupWorktreeSyncHook();
8591
8695
  } catch {}
8592
8696
  }
8593
- async function runHookSetup(argv) {
8594
- if (argv.includes("--help") || argv.includes("-h")) {
8595
- process.stdout.write([
8596
- "Usage: kobe hook setup [--global | --repo <path> | --off]",
8597
- "",
8598
- "Syncs an external `claude --worktree` into kobe as a task. This is ON",
8599
- "by default (installed globally into ~/.claude on launch). Use this to",
8600
- "move it to one repo (--repo <path>) or to turn it OFF (--off).",
8601
- ""
8602
- ].join(`
8603
- `));
8604
- return;
8605
- }
8606
- const off = argv.includes("--off");
8607
- const repoIdx = argv.indexOf("--repo");
8608
- const repoPath = repoIdx !== -1 ? argv[repoIdx + 1] : undefined;
8609
- if (repoIdx !== -1 && !repoPath) {
8610
- process.stderr.write(`kobe hook setup: --repo requires a path
8611
- `);
8612
- process.exit(2);
8613
- }
8697
+ async function cleanupWorktreeSyncHook() {
8614
8698
  const adapters = worktreeSyncAdapters();
8615
- if (adapters.length === 0) {
8616
- process.stdout.write(`kobe hook setup: no engine supports external worktree sync \u2014 nothing to do
8617
- `);
8699
+ if (adapters.length === 0)
8618
8700
  return;
8619
- }
8620
- const prevPath = persistedSyncPath(getPersistedString(SYNC_SETTING_KEY));
8621
- if (off) {
8622
- if (prevPath) {
8623
- for (const a of adapters)
8624
- await a.removeWorktreeSyncHook(prevPath);
8625
- }
8626
- setPersistedString(SYNC_SETTING_KEY, "off");
8627
- process.stdout.write(`kobe hook setup: external worktree sync disabled${prevPath ? ` (removed from ${prevPath})` : ""}
8628
- `);
8629
- return;
8630
- }
8631
- const scope = repoPath ? { kind: "repo", path: repoPath } : { kind: "global" };
8632
- const path6 = syncSettingsPath(scope);
8633
- if (prevPath && prevPath !== path6) {
8634
- for (const a of adapters)
8635
- await a.removeWorktreeSyncHook(prevPath);
8636
- }
8701
+ const stored = getPersistedString(SYNC_SETTING_KEY);
8702
+ const paths = new Set([globalSettingsPath()]);
8703
+ const prev = persistedSyncPath(stored);
8704
+ if (prev)
8705
+ paths.add(prev);
8637
8706
  for (const a of adapters)
8638
- await a.installWorktreeSyncHook(path6);
8639
- setPersistedString(SYNC_SETTING_KEY, path6);
8707
+ for (const p of paths)
8708
+ await a.removeWorktreeSyncHook(p);
8709
+ if (stored !== "off")
8710
+ setPersistedString(SYNC_SETTING_KEY, "off");
8711
+ }
8712
+ async function runHookSetup(_argv) {
8713
+ await cleanupWorktreeSyncHook();
8640
8714
  process.stdout.write([
8641
- `kobe hook setup: external worktree sync enabled (${scope.kind}) \u2014 wrote ${path6}`,
8642
- "New `claude --worktree` worktrees will appear as kobe tasks.",
8715
+ "kobe hook setup is deprecated and now a no-op (cleanup only).",
8716
+ "",
8717
+ "The old external-worktree sync used a global WorktreeCreate hook, which is",
8718
+ "a VCS provider hook \u2014 its presence broke `claude --worktree` / EnterWorktree",
8719
+ "in every repo. Any hook kobe previously installed has been removed.",
8720
+ "",
8721
+ "Sync is now automatic: a `claude --worktree` (or any session) started in a",
8722
+ "worktree under a repo kobe already tracks is adopted as a task on launch.",
8723
+ "To adopt existing worktrees on demand, use the New Task dialog or `kobe adopt`.",
8643
8724
  ""
8644
8725
  ].join(`
8645
8726
  `));
8646
8727
  }
8647
- var SYNC_SETTING_KEY = "externalWorktreeSync";
8728
+ var WORKTREE_CREATED_VERB = "worktree-created", SYNC_SETTING_KEY = "externalWorktreeSync";
8648
8729
  var init_hook_cmd = __esm(() => {
8649
8730
  init_daemon_process();
8650
8731
  init_hook_adapter2();
@@ -15988,7 +16069,7 @@ var init_focus = __esm(() => {
15988
16069
 
15989
16070
  // src/tui/context/kv.tsx
15990
16071
  import { mkdirSync as mkdirSync4, readFileSync as readFileSync9, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
15991
- import { dirname as dirname8 } from "path";
16072
+ import { dirname as dirname7 } from "path";
15992
16073
  function loadInitial() {
15993
16074
  const statePath2 = kvStatePath();
15994
16075
  try {
@@ -16016,7 +16097,7 @@ var init_kv = __esm(() => {
16016
16097
  function writeNow(label) {
16017
16098
  const statePath2 = kvStatePath();
16018
16099
  try {
16019
- mkdirSync4(dirname8(statePath2), {
16100
+ mkdirSync4(dirname7(statePath2), {
16020
16101
  recursive: true
16021
16102
  });
16022
16103
  const tmp = `${statePath2}.tmp`;
@@ -16071,7 +16152,7 @@ var init_kv = __esm(() => {
16071
16152
  }
16072
16153
  const statePath2 = kvStatePath();
16073
16154
  try {
16074
- mkdirSync4(dirname8(statePath2), {
16155
+ mkdirSync4(dirname7(statePath2), {
16075
16156
  recursive: true
16076
16157
  });
16077
16158
  const tmp = `${statePath2}.tmp`;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@sma1lboy/kobe",
4
- "version": "0.7.9",
4
+ "version": "0.7.11",
5
5
  "description": "TUI orchestrator for Claude Code (codename)",
6
6
  "type": "module",
7
7
  "packageManager": "bun@1.3.13",