@sma1lboy/kobe 0.7.8 → 0.7.9

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 +244 -240
  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.8",
73
+ version: "0.7.9",
74
74
  description: "TUI orchestrator for Claude Code (codename)",
75
75
  type: "module",
76
76
  packageManager: "bun@1.3.13",
@@ -4997,6 +4997,29 @@ var init_auto_title_poller = __esm(() => {
4997
4997
  init_chat_tab_naming();
4998
4998
  });
4999
4999
 
5000
+ // src/daemon/cwd-task.ts
5001
+ function normalize(p) {
5002
+ return p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p;
5003
+ }
5004
+ function isAncestorOrSelf(wt, cwd) {
5005
+ return cwd === wt || cwd.startsWith(`${wt}/`);
5006
+ }
5007
+ function matchTaskByCwd(tasks, cwd) {
5008
+ const target = normalize(cwd);
5009
+ let bestId;
5010
+ let bestLen = -1;
5011
+ for (const t of tasks) {
5012
+ if (!t.worktreePath)
5013
+ continue;
5014
+ const wt = normalize(t.worktreePath);
5015
+ if (isAncestorOrSelf(wt, target) && wt.length > bestLen) {
5016
+ bestLen = wt.length;
5017
+ bestId = t.id;
5018
+ }
5019
+ }
5020
+ return bestId;
5021
+ }
5022
+
5000
5023
  // src/daemon/event-bus.ts
5001
5024
  class DaemonEventBus {
5002
5025
  last = new Map;
@@ -5317,10 +5340,14 @@ async function startDaemonServer(orch, options = {}) {
5317
5340
  return {};
5318
5341
  }
5319
5342
  case "engine.reportEvent": {
5320
- const taskId = requireString(payload, "taskId");
5321
5343
  const kind = requireString(payload, "kind");
5322
5344
  if (!isEngineActivityKind(kind))
5323
5345
  throw new Error(`unknown engine event kind: ${kind}`);
5346
+ const explicitId = optionalString(payload, "taskId");
5347
+ const cwd = optionalString(payload, "cwd");
5348
+ const taskId = explicitId ?? (cwd ? matchTaskByCwd(orch.listTasks(), cwd) : undefined);
5349
+ if (!taskId)
5350
+ return {};
5324
5351
  const detail = optionalActivityDetail(payload);
5325
5352
  reportActivity(taskId, kind, detail);
5326
5353
  return {};
@@ -5863,172 +5890,6 @@ var init_prompt_delivery = __esm(() => {
5863
5890
  init_client2();
5864
5891
  });
5865
5892
 
5866
- // src/engine/claude-code-local/hook-adapter.ts
5867
- import { existsSync as existsSync2 } from "fs";
5868
- import { appendFile as appendFile3, mkdir as mkdir5, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
5869
- import { dirname as dirname6, join as join4 } from "path";
5870
- function isObject6(v) {
5871
- return !!v && typeof v === "object" && !Array.isArray(v);
5872
- }
5873
- async function readJsonObject(path6) {
5874
- try {
5875
- const parsed = JSON.parse(await readFile6(path6, "utf8"));
5876
- return isObject6(parsed) ? parsed : {};
5877
- } catch {
5878
- return {};
5879
- }
5880
- }
5881
- function isKobeWorktreeSyncGroup(group) {
5882
- if (!isObject6(group) || !Array.isArray(group.hooks))
5883
- return false;
5884
- return group.hooks.some((h) => isObject6(h) && typeof h.command === "string" && h.command.includes(WORKTREE_SYNC_MARKER));
5885
- }
5886
- function mergeWorktreeSyncHook(current, command) {
5887
- const { hooks: rawHooks, ...restSettings } = current;
5888
- const { WorktreeCreate, ...otherHooks } = isObject6(rawHooks) ? rawHooks : {};
5889
- const prior = Array.isArray(WorktreeCreate) ? WorktreeCreate : [];
5890
- const kept = prior.filter((g) => !isKobeWorktreeSyncGroup(g));
5891
- if (command !== null)
5892
- kept.push({ hooks: [{ type: "command", command }] });
5893
- const nextHooks = { ...otherHooks };
5894
- if (kept.length > 0)
5895
- nextHooks.WorktreeCreate = kept;
5896
- return Object.keys(nextHooks).length > 0 ? { ...restSettings, hooks: nextHooks } : { ...restSettings };
5897
- }
5898
- function buildClaudeHooks(taskId, inv = kobeCliInvocation()) {
5899
- const out = {};
5900
- for (const { event, matcher, verb } of EVENT_MAP) {
5901
- const command = shellQuoteArgv([...inv, "hook", verb, "--task-id", taskId]);
5902
- const group = { hooks: [{ type: "command", command }] };
5903
- if (matcher)
5904
- group.matcher = matcher;
5905
- out[event] = [group];
5906
- }
5907
- return out;
5908
- }
5909
- function mergeClaudeHooks(existing, kobeHooks) {
5910
- const merged = { ...existing };
5911
- for (const event of KOBE_HOOK_EVENTS)
5912
- merged[event] = kobeHooks[event];
5913
- return merged;
5914
- }
5915
-
5916
- class ClaudeHookAdapter {
5917
- vendor = "claude";
5918
- supportsHooks() {
5919
- return true;
5920
- }
5921
- supportsWorktreeSync() {
5922
- return true;
5923
- }
5924
- async installWorktreeSyncHook(settingsFilePath) {
5925
- await this.editWorktreeSyncHook(settingsFilePath, true);
5926
- }
5927
- async removeWorktreeSyncHook(settingsFilePath) {
5928
- await this.editWorktreeSyncHook(settingsFilePath, false);
5929
- }
5930
- async editWorktreeSyncHook(settingsFilePath, install) {
5931
- const current = await readJsonObject(settingsFilePath);
5932
- const command = install ? shellQuoteArgv([...kobeCliInvocation(), "hook", "worktree-created"]) : null;
5933
- const next = mergeWorktreeSyncHook(current, command);
5934
- if (JSON.stringify(next) === JSON.stringify(current))
5935
- return;
5936
- await mkdir5(dirname6(settingsFilePath), { recursive: true });
5937
- await writeFile4(settingsFilePath, `${JSON.stringify(next, null, 2)}
5938
- `);
5939
- }
5940
- async installTaskHooks(ctx) {
5941
- try {
5942
- const claudeDir = join4(ctx.worktreeDir, ".claude");
5943
- const settingsPath = join4(claudeDir, "settings.local.json");
5944
- await mkdir5(claudeDir, { recursive: true });
5945
- let current = {};
5946
- try {
5947
- const raw = await readFile6(settingsPath, "utf8");
5948
- const parsed = JSON.parse(raw);
5949
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
5950
- current = parsed;
5951
- } catch {}
5952
- const existingHooks = current.hooks && typeof current.hooks === "object" && !Array.isArray(current.hooks) ? current.hooks : {};
5953
- current.hooks = mergeClaudeHooks(existingHooks, buildClaudeHooks(ctx.taskId));
5954
- await writeFile4(settingsPath, `${JSON.stringify(current, null, 2)}
5955
- `);
5956
- await hideFromGit(ctx.worktreeDir, ".claude/settings.local.json");
5957
- } catch {}
5958
- }
5959
- }
5960
- async function hideFromGit(worktreeDir, relPath) {
5961
- if (hiddenWorktrees.has(worktreeDir))
5962
- return;
5963
- try {
5964
- const proc = Bun.spawn(["git", "-C", worktreeDir, "rev-parse", "--git-common-dir"], {
5965
- stdout: "pipe",
5966
- stderr: "ignore"
5967
- });
5968
- const [out, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
5969
- if (code !== 0)
5970
- return;
5971
- let commonDir = out.trim();
5972
- if (!commonDir)
5973
- return;
5974
- if (!commonDir.startsWith("/"))
5975
- commonDir = join4(worktreeDir, commonDir);
5976
- const excludePath = join4(commonDir, "info", "exclude");
5977
- const existing = existsSync2(excludePath) ? await readFile6(excludePath, "utf8") : "";
5978
- if (existing.split(`
5979
- `).some((l) => l.trim() === relPath)) {
5980
- hiddenWorktrees.add(worktreeDir);
5981
- return;
5982
- }
5983
- await mkdir5(join4(commonDir, "info"), { recursive: true });
5984
- await appendFile3(excludePath, `${existing.endsWith(`
5985
- `) || existing === "" ? "" : `
5986
- `}${relPath}
5987
- `);
5988
- hiddenWorktrees.add(worktreeDir);
5989
- } catch {}
5990
- }
5991
- var EVENT_MAP, KOBE_HOOK_EVENTS, WORKTREE_SYNC_MARKER = "worktree-created", hiddenWorktrees;
5992
- var init_hook_adapter = __esm(() => {
5993
- init_invocation();
5994
- EVENT_MAP = [
5995
- { event: "SessionStart", verb: "session-start" },
5996
- { event: "UserPromptSubmit", verb: "turn-start" },
5997
- { event: "Stop", verb: "turn-complete" },
5998
- { event: "StopFailure", verb: "turn-failed" },
5999
- { event: "Notification", matcher: "permission_prompt", verb: "awaiting-input" },
6000
- { event: "SessionEnd", verb: "session-end" }
6001
- ];
6002
- KOBE_HOOK_EVENTS = EVENT_MAP.map((e) => e.event);
6003
- hiddenWorktrees = new Set;
6004
- });
6005
-
6006
- // src/engine/hook-adapter.ts
6007
- function createEngineHookAdapter(vendor) {
6008
- if (vendor === "claude")
6009
- return new ClaudeHookAdapter;
6010
- return new NoopHookAdapter(vendor);
6011
- }
6012
-
6013
- class NoopHookAdapter {
6014
- vendor;
6015
- constructor(vendor) {
6016
- this.vendor = vendor;
6017
- }
6018
- supportsHooks() {
6019
- return false;
6020
- }
6021
- async installTaskHooks() {}
6022
- supportsWorktreeSync() {
6023
- return false;
6024
- }
6025
- async installWorktreeSyncHook() {}
6026
- async removeWorktreeSyncHook() {}
6027
- }
6028
- var init_hook_adapter2 = __esm(() => {
6029
- init_hook_adapter();
6030
- });
6031
-
6032
5893
  // src/tui/panes/terminal/tmux.ts
6033
5894
  var exports_tmux = {};
6034
5895
  __export(exports_tmux, {
@@ -6079,12 +5940,6 @@ async function ensureSession(opts) {
6079
5940
  }
6080
5941
  }
6081
5942
  async function ensureSessionImpl(opts) {
6082
- if (opts.taskId && opts.cwd && opts.cwd.includes(`/${KOBE_WORKTREE_ROOT_SUBPATH}/`)) {
6083
- await createEngineHookAdapter(coerceVendorId(opts.vendor)).installTaskHooks({
6084
- worktreeDir: opts.cwd,
6085
- taskId: opts.taskId
6086
- });
6087
- }
6088
5943
  if (await sessionExists(opts.name)) {
6089
5944
  const sessionOptions = await getSessionOptions(opts.name, ["@kobe_worktree", "@kobe_vendor"]);
6090
5945
  const taggedWorktree = sessionOptions["@kobe_worktree"] ?? "";
@@ -6499,10 +6354,8 @@ async function quickCreate(session) {
6499
6354
  var CHAT_TAB_SWITCH_BINDINGS, CHAT_TAB_CLOSE_BINDING, CHAT_TAB_RENAME_BINDING, CHAT_TAB_ENGINE_PROMPT, CHAT_TAB_CHOOSE_ENGINE_BINDINGS, CHAT_TAB_STATE_OPTION = "@kobe_tab_state", PANE_VERSION_OPTION = "@kobe_pane_version", CHAT_TAB_STATUS_FORMAT = "#{?#{==:#{@kobe_tab_state},running},\u25CF,#{?#{==:#{@kobe_tab_state},done},\u2713,#{?#{==:#{@kobe_tab_state},error},!,#{?#{==:#{@kobe_tab_state},unknown},?,\u25CB}}}} #I:#W", CHAT_TAB_STATUS_CURRENT_FORMAT, ensureSessionLocks;
6500
6355
  var init_tmux = __esm(() => {
6501
6356
  init_invocation();
6502
- init_hook_adapter2();
6503
6357
  init_interactive_command();
6504
6358
  init_env();
6505
- init_paths();
6506
6359
  init_client2();
6507
6360
  init_prompt_delivery();
6508
6361
  init_vendor();
@@ -6545,14 +6398,14 @@ var exports_repo_init = {};
6545
6398
  __export(exports_repo_init, {
6546
6399
  resolveRepoInit: () => resolveRepoInit
6547
6400
  });
6548
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
6549
- import { join as join5 } from "path";
6401
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
6402
+ import { join as join4 } from "path";
6550
6403
  function repoFileScript(worktreePath) {
6551
- return existsSync3(join5(worktreePath, INIT_SCRIPT_REL)) ? `sh ${INIT_SCRIPT_REL}` : undefined;
6404
+ return existsSync2(join4(worktreePath, INIT_SCRIPT_REL)) ? `sh ${INIT_SCRIPT_REL}` : undefined;
6552
6405
  }
6553
6406
  function repoFilePrompt(worktreePath) {
6554
- const p = join5(worktreePath, INIT_PROMPT_REL);
6555
- if (!existsSync3(p))
6407
+ const p = join4(worktreePath, INIT_PROMPT_REL);
6408
+ if (!existsSync2(p))
6556
6409
  return;
6557
6410
  try {
6558
6411
  const text = readFileSync3(p, "utf8");
@@ -6573,8 +6426,8 @@ function resolveRepoInit(repoRoot, worktreePath) {
6573
6426
  var INIT_SCRIPT_REL, INIT_PROMPT_REL;
6574
6427
  var init_repo_init = __esm(() => {
6575
6428
  init_repos();
6576
- INIT_SCRIPT_REL = join5(".kobe", "init.sh");
6577
- INIT_PROMPT_REL = join5(".kobe", "init-prompt.md");
6429
+ INIT_SCRIPT_REL = join4(".kobe", "init.sh");
6430
+ INIT_PROMPT_REL = join4(".kobe", "init-prompt.md");
6578
6431
  });
6579
6432
 
6580
6433
  // src/tui/panes/sidebar/worktree-changes.ts
@@ -7586,9 +7439,9 @@ var init_schema = () => {};
7586
7439
 
7587
7440
  // src/tui/context/theme/loader.ts
7588
7441
  import { readFileSync as readFileSync4, readdirSync } from "fs";
7589
- import { join as join6 } from "path";
7442
+ import { join as join5 } from "path";
7590
7443
  function userThemesDir() {
7591
- return join6(kobeStateDir(), "themes");
7444
+ return join5(kobeStateDir(), "themes");
7592
7445
  }
7593
7446
  function loadUserThemes() {
7594
7447
  const dir = userThemesDir();
@@ -7602,7 +7455,7 @@ function loadUserThemes() {
7602
7455
  for (const file of entries) {
7603
7456
  if (!file.endsWith(".json"))
7604
7457
  continue;
7605
- const path6 = join6(dir, file);
7458
+ const path6 = join5(dir, file);
7606
7459
  let parsed;
7607
7460
  try {
7608
7461
  const text = readFileSync4(path6, "utf8");
@@ -7632,8 +7485,8 @@ var exports_theme = {};
7632
7485
  __export(exports_theme, {
7633
7486
  runThemeSubcommand: () => runThemeSubcommand
7634
7487
  });
7635
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, readdirSync as readdirSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
7636
- import { basename as basename3, join as join7, resolve as resolve5 } from "path";
7488
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, readdirSync as readdirSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
7489
+ import { basename as basename3, join as join6, resolve as resolve5 } from "path";
7637
7490
  function fail2(message) {
7638
7491
  process.stderr.write(`kobe theme: ${message}
7639
7492
  `);
@@ -7664,7 +7517,7 @@ function listThemes() {
7664
7517
  } else {
7665
7518
  for (const f of userFiles) {
7666
7519
  const name = f.slice(0, -".json".length);
7667
- const path6 = join7(dir, f);
7520
+ const path6 = join6(dir, f);
7668
7521
  const overridesBundled = BUNDLED_NAMES.includes(name) ? " (overrides built-in)" : "";
7669
7522
  lines.push(` ${name}${overridesBundled} ${path6}`);
7670
7523
  }
@@ -7754,8 +7607,8 @@ async function addTheme(args) {
7754
7607
  }
7755
7608
  const dir = userThemesDir();
7756
7609
  mkdirSync3(dir, { recursive: true });
7757
- const dest = join7(dir, `${name}.json`);
7758
- if (existsSync4(dest) && !opts.force) {
7610
+ const dest = join6(dir, `${name}.json`);
7611
+ if (existsSync3(dest) && !opts.force) {
7759
7612
  fail2(`${dest} already exists (pass --force to overwrite)`);
7760
7613
  }
7761
7614
  writeFileSync2(dest, `${JSON.stringify(result.theme, null, 2)}
@@ -7772,8 +7625,8 @@ function removeTheme(args) {
7772
7625
  if (BUNDLED_NAMES.includes(name)) {
7773
7626
  fail2(`"${name}" is a built-in theme and cannot be removed`);
7774
7627
  }
7775
- const dest = join7(userThemesDir(), `${name}.json`);
7776
- if (!existsSync4(dest)) {
7628
+ const dest = join6(userThemesDir(), `${name}.json`);
7629
+ if (!existsSync3(dest)) {
7777
7630
  fail2(`no user theme named "${name}" (looked for ${dest})`);
7778
7631
  }
7779
7632
  unlinkSync(dest);
@@ -7956,9 +7809,9 @@ var init_daemon_cmd = __esm(() => {
7956
7809
  });
7957
7810
 
7958
7811
  // src/lib/skill-install.ts
7959
- import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
7812
+ import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
7960
7813
  import { homedir as homedir9 } from "os";
7961
- import { join as join8 } from "path";
7814
+ import { join as join7 } from "path";
7962
7815
  function npxSkillsArgv(opts = {}) {
7963
7816
  return ["skills", "add", SKILL_SOURCE_SLUG, "--skill", "kobe", "--agent", opts.agent ?? DEFAULT_SKILL_AGENT];
7964
7817
  }
@@ -7968,14 +7821,14 @@ function npxSkillsCommand(opts = {}) {
7968
7821
  function kobeSkillPaths(opts = {}) {
7969
7822
  const home = opts.home ?? homedir9();
7970
7823
  const cwd = opts.cwd ?? process.cwd();
7971
- return [join8(home, SKILL_REL_PATH), join8(cwd, SKILL_REL_PATH)];
7824
+ return [join7(home, SKILL_REL_PATH), join7(cwd, SKILL_REL_PATH)];
7972
7825
  }
7973
7826
  function parseSkillVersion(content) {
7974
7827
  const m = content.match(/kobe-skill-version:\s*(\d+)/);
7975
7828
  return m ? Number.parseInt(m[1], 10) : null;
7976
7829
  }
7977
7830
  function kobeSkillState(opts) {
7978
- const path6 = kobeSkillPaths(opts).find((p) => existsSync5(p));
7831
+ const path6 = kobeSkillPaths(opts).find((p) => existsSync4(p));
7979
7832
  if (!path6) {
7980
7833
  return { installed: false, installedVersion: null, currentVersion: KOBE_SKILL_VERSION, stale: false };
7981
7834
  }
@@ -8027,9 +7880,9 @@ __export(exports_maintenance, {
8027
7880
  runReloadSubcommand: () => runReloadSubcommand,
8028
7881
  runDoctorSubcommand: () => runDoctorSubcommand
8029
7882
  });
8030
- import { existsSync as existsSync6, readFileSync as readFileSync7, statSync } from "fs";
7883
+ import { existsSync as existsSync5, readFileSync as readFileSync7, statSync } from "fs";
8031
7884
  import { unlink as unlink6 } from "fs/promises";
8032
- import { join as join9 } from "path";
7885
+ import { join as join8 } from "path";
8033
7886
  import { createInterface } from "readline";
8034
7887
  function isProcessAlive2(pid) {
8035
7888
  try {
@@ -8126,7 +7979,7 @@ async function runDoctorSubcommand(argv = []) {
8126
7979
  const socketPath = defaultDaemonSocketPath();
8127
7980
  const pidPath = defaultDaemonPidPath();
8128
7981
  const logPath = defaultDaemonLogPath();
8129
- const tasksPath = join9(kobeStateDir(), "tasks.json");
7982
+ const tasksPath = join8(kobeStateDir(), "tasks.json");
8130
7983
  const statePath2 = kvStatePath();
8131
7984
  const out = ["kobe doctor", ` home: ${homeDir()}`, ` socket: ${socketPath}`, ""];
8132
7985
  const status = await probeDaemonStatus(socketPath);
@@ -8154,7 +8007,7 @@ async function runDoctorSubcommand(argv = []) {
8154
8007
  } else {
8155
8008
  out.push("daemon: \u2717 not running (no pidfile)");
8156
8009
  }
8157
- if (existsSync6(socketPath))
8010
+ if (existsSync5(socketPath))
8158
8011
  out.push(` orphan socket file present: ${socketPath}`);
8159
8012
  const tail = tailFile(logPath, 8);
8160
8013
  if (tail) {
@@ -8245,7 +8098,7 @@ async function runResetSubcommand(argv) {
8245
8098
  const yes = argv.includes("--yes") || argv.includes("-y");
8246
8099
  const socketPath = defaultDaemonSocketPath();
8247
8100
  const pidPath = defaultDaemonPidPath();
8248
- const tasksPath = join9(kobeStateDir(), "tasks.json");
8101
+ const tasksPath = join8(kobeStateDir(), "tasks.json");
8249
8102
  const statePath2 = kvStatePath();
8250
8103
  console.log("kobe reset will:");
8251
8104
  console.log(" \u2022 stop the kobe daemon (graceful \u2192 SIGTERM \u2192 SIGKILL)");
@@ -8449,14 +8302,153 @@ var init_skill_cmd = __esm(() => {
8449
8302
  SKILL_VERBS = ["install", "status", "command"];
8450
8303
  });
8451
8304
 
8305
+ // src/engine/claude-code-local/hook-adapter.ts
8306
+ import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
8307
+ import { dirname as dirname6 } from "path";
8308
+ function isObject6(v) {
8309
+ return !!v && typeof v === "object" && !Array.isArray(v);
8310
+ }
8311
+ async function readJsonObject(path6) {
8312
+ try {
8313
+ const parsed = JSON.parse(await readFile6(path6, "utf8"));
8314
+ return isObject6(parsed) ? parsed : {};
8315
+ } catch {
8316
+ return {};
8317
+ }
8318
+ }
8319
+ function isKobeWorktreeSyncGroup(group) {
8320
+ if (!isObject6(group) || !Array.isArray(group.hooks))
8321
+ return false;
8322
+ return group.hooks.some((h) => isObject6(h) && typeof h.command === "string" && h.command.includes(WORKTREE_SYNC_MARKER));
8323
+ }
8324
+ function isKobeActivityGroup(group) {
8325
+ if (!isObject6(group) || !Array.isArray(group.hooks))
8326
+ return false;
8327
+ return group.hooks.some((h) => isObject6(h) && typeof h.command === "string" && ACTIVITY_MARKERS.some((m) => h.command.includes(m)));
8328
+ }
8329
+ function mergeWorktreeSyncHook(current, command) {
8330
+ const { hooks: rawHooks, ...restSettings } = current;
8331
+ const { WorktreeCreate, ...otherHooks } = isObject6(rawHooks) ? rawHooks : {};
8332
+ const prior = Array.isArray(WorktreeCreate) ? WorktreeCreate : [];
8333
+ const kept = prior.filter((g) => !isKobeWorktreeSyncGroup(g));
8334
+ if (command !== null)
8335
+ kept.push({ hooks: [{ type: "command", command }] });
8336
+ const nextHooks = { ...otherHooks };
8337
+ if (kept.length > 0)
8338
+ nextHooks.WorktreeCreate = kept;
8339
+ return Object.keys(nextHooks).length > 0 ? { ...restSettings, hooks: nextHooks } : { ...restSettings };
8340
+ }
8341
+ function buildClaudeHooks(inv = kobeCliInvocation()) {
8342
+ const out = {};
8343
+ for (const { event, matcher, verb } of EVENT_MAP) {
8344
+ const command = shellQuoteArgv([...inv, "hook", verb]);
8345
+ const group = { hooks: [{ type: "command", command }] };
8346
+ if (matcher)
8347
+ group.matcher = matcher;
8348
+ out[event] = [group];
8349
+ }
8350
+ return out;
8351
+ }
8352
+ function mergeActivityHooks(current, install, inv = kobeCliInvocation()) {
8353
+ const { hooks: rawHooks, ...restSettings } = current;
8354
+ const hooks = isObject6(rawHooks) ? { ...rawHooks } : {};
8355
+ const built = install ? buildClaudeHooks(inv) : {};
8356
+ for (const { event } of EVENT_MAP) {
8357
+ const prior = Array.isArray(hooks[event]) ? hooks[event] : [];
8358
+ const kept = prior.filter((g) => !isKobeActivityGroup(g));
8359
+ if (install && Array.isArray(built[event]))
8360
+ kept.push(...built[event]);
8361
+ if (kept.length > 0)
8362
+ hooks[event] = kept;
8363
+ else
8364
+ delete hooks[event];
8365
+ }
8366
+ return Object.keys(hooks).length > 0 ? { ...restSettings, hooks } : { ...restSettings };
8367
+ }
8368
+
8369
+ class ClaudeHookAdapter {
8370
+ vendor = "claude";
8371
+ supportsHooks() {
8372
+ return true;
8373
+ }
8374
+ supportsWorktreeSync() {
8375
+ return true;
8376
+ }
8377
+ async installActivityHooks(settingsFilePath) {
8378
+ await this.editSettings(settingsFilePath, (cur) => mergeActivityHooks(cur, true));
8379
+ }
8380
+ async removeActivityHooks(settingsFilePath) {
8381
+ await this.editSettings(settingsFilePath, (cur) => mergeActivityHooks(cur, false));
8382
+ }
8383
+ async installWorktreeSyncHook(settingsFilePath) {
8384
+ const command = shellQuoteArgv([...kobeCliInvocation(), "hook", "worktree-created"]);
8385
+ await this.editSettings(settingsFilePath, (cur) => mergeWorktreeSyncHook(cur, command));
8386
+ }
8387
+ async removeWorktreeSyncHook(settingsFilePath) {
8388
+ await this.editSettings(settingsFilePath, (cur) => mergeWorktreeSyncHook(cur, null));
8389
+ }
8390
+ async editSettings(settingsFilePath, transform) {
8391
+ try {
8392
+ const current = await readJsonObject(settingsFilePath);
8393
+ const next = transform(current);
8394
+ if (JSON.stringify(next) === JSON.stringify(current))
8395
+ return;
8396
+ await mkdir5(dirname6(settingsFilePath), { recursive: true });
8397
+ await writeFile4(settingsFilePath, `${JSON.stringify(next, null, 2)}
8398
+ `);
8399
+ } catch {}
8400
+ }
8401
+ }
8402
+ var EVENT_MAP, KOBE_HOOK_EVENTS, ACTIVITY_MARKERS, WORKTREE_SYNC_MARKER = "worktree-created";
8403
+ var init_hook_adapter = __esm(() => {
8404
+ init_invocation();
8405
+ EVENT_MAP = [
8406
+ { event: "SessionStart", verb: "session-start" },
8407
+ { event: "UserPromptSubmit", verb: "turn-start" },
8408
+ { event: "Stop", verb: "turn-complete" },
8409
+ { event: "StopFailure", verb: "turn-failed" },
8410
+ { event: "Notification", matcher: "permission_prompt", verb: "awaiting-input" },
8411
+ { event: "SessionEnd", verb: "session-end" }
8412
+ ];
8413
+ KOBE_HOOK_EVENTS = EVENT_MAP.map((e) => e.event);
8414
+ ACTIVITY_MARKERS = EVENT_MAP.map((e) => shellQuoteArgv(["hook", e.verb]));
8415
+ });
8416
+
8417
+ // src/engine/hook-adapter.ts
8418
+ function createEngineHookAdapter(vendor) {
8419
+ if (vendor === "claude")
8420
+ return new ClaudeHookAdapter;
8421
+ return new NoopHookAdapter(vendor);
8422
+ }
8423
+
8424
+ class NoopHookAdapter {
8425
+ vendor;
8426
+ constructor(vendor) {
8427
+ this.vendor = vendor;
8428
+ }
8429
+ supportsHooks() {
8430
+ return false;
8431
+ }
8432
+ async installActivityHooks() {}
8433
+ async removeActivityHooks() {}
8434
+ supportsWorktreeSync() {
8435
+ return false;
8436
+ }
8437
+ async installWorktreeSyncHook() {}
8438
+ async removeWorktreeSyncHook() {}
8439
+ }
8440
+ var init_hook_adapter2 = __esm(() => {
8441
+ init_hook_adapter();
8442
+ });
8443
+
8452
8444
  // src/cli/hook-cmd.ts
8453
8445
  var exports_hook_cmd = {};
8454
8446
  __export(exports_hook_cmd, {
8455
8447
  runHookSubcommand: () => runHookSubcommand,
8456
- ensureDefaultWorktreeSync: () => ensureDefaultWorktreeSync
8448
+ ensureGlobalKobeHooks: () => ensureGlobalKobeHooks
8457
8449
  });
8458
8450
  import { homedir as homedir10 } from "os";
8459
- import { dirname as dirname7, join as join10, resolve as resolve6 } from "path";
8451
+ import { dirname as dirname7, join as join9, resolve as resolve6 } from "path";
8460
8452
  async function readStdinPayload() {
8461
8453
  try {
8462
8454
  const text = await Promise.race([
@@ -8502,10 +8494,9 @@ async function runHookSubcommand(argv) {
8502
8494
  }
8503
8495
  if (!verb || !isEngineActivityKind(verb))
8504
8496
  return;
8505
- const taskId = flagValue(rest, "--task-id");
8506
- if (!taskId)
8507
- return;
8508
8497
  const payload = await readStdinPayload();
8498
+ const taskId = flagValue(rest, "--task-id");
8499
+ const cwd = typeof payload.cwd === "string" && payload.cwd ? payload.cwd : process.cwd();
8509
8500
  let detail;
8510
8501
  if (verb === "turn-failed") {
8511
8502
  detail = { failure: failureFromErrorType(payload.error_type) };
@@ -8516,7 +8507,11 @@ async function runHookSubcommand(argv) {
8516
8507
  if (!client)
8517
8508
  return;
8518
8509
  try {
8519
- await client.request("engine.reportEvent", { taskId, kind: verb, ...detail ? { detail } : {} });
8510
+ await client.request("engine.reportEvent", {
8511
+ ...taskId ? { taskId } : { cwd },
8512
+ kind: verb,
8513
+ ...detail ? { detail } : {}
8514
+ });
8520
8515
  } finally {
8521
8516
  client.close();
8522
8517
  }
@@ -8556,12 +8551,18 @@ async function deriveRepoRoot(worktreePath) {
8556
8551
  }
8557
8552
  function syncSettingsPath(scope) {
8558
8553
  if (scope.kind === "repo")
8559
- return join10(resolve6(scope.path), ".claude", "settings.json");
8560
- return join10(homedir10(), ".claude", "settings.json");
8554
+ return join9(resolve6(scope.path), ".claude", "settings.json");
8555
+ return globalSettingsPath();
8561
8556
  }
8562
8557
  function worktreeSyncAdapters() {
8563
8558
  return ALL_VENDORS.map((v) => createEngineHookAdapter(v)).filter((a) => a.supportsWorktreeSync());
8564
8559
  }
8560
+ function activityHookAdapters() {
8561
+ return ALL_VENDORS.map((v) => createEngineHookAdapter(v)).filter((a) => a.supportsHooks());
8562
+ }
8563
+ function globalSettingsPath() {
8564
+ return join9(homedir10(), ".claude", "settings.json");
8565
+ }
8565
8566
  function persistedSyncPath(stored) {
8566
8567
  if (!stored || stored === "off")
8567
8568
  return;
@@ -8571,16 +8572,19 @@ function persistedSyncPath(stored) {
8571
8572
  return syncSettingsPath({ kind: "repo", path: stored.slice(5) });
8572
8573
  return stored;
8573
8574
  }
8574
- async function ensureDefaultWorktreeSync() {
8575
+ async function ensureGlobalKobeHooks() {
8575
8576
  try {
8577
+ const globalPath = globalSettingsPath();
8578
+ for (const a of activityHookAdapters())
8579
+ await a.installActivityHooks(globalPath);
8576
8580
  const stored = getPersistedString(SYNC_SETTING_KEY);
8577
8581
  if (stored === "off")
8578
8582
  return;
8579
- const adapters = worktreeSyncAdapters();
8580
- if (adapters.length === 0)
8583
+ const syncAdapters = worktreeSyncAdapters();
8584
+ if (syncAdapters.length === 0)
8581
8585
  return;
8582
8586
  const path6 = persistedSyncPath(stored) ?? syncSettingsPath({ kind: "global" });
8583
- for (const a of adapters)
8587
+ for (const a of syncAdapters)
8584
8588
  await a.installWorktreeSyncHook(path6);
8585
8589
  if (!stored)
8586
8590
  setPersistedString(SYNC_SETTING_KEY, path6);
@@ -13854,7 +13858,7 @@ var init_rename_task_dialog = __esm(() => {
13854
13858
 
13855
13859
  // src/engine/claude-code-local/binary.ts
13856
13860
  import { spawnSync as spawnSync5 } from "child_process";
13857
- import { existsSync as existsSync8, statSync as statSync3 } from "fs";
13861
+ import { existsSync as existsSync7, statSync as statSync3 } from "fs";
13858
13862
  import { homedir as homedir12 } from "os";
13859
13863
  import path7 from "path";
13860
13864
  async function findClaudeBinary(deps = defaultDeps4) {
@@ -13941,7 +13945,7 @@ var init_binary = __esm(() => {
13941
13945
  return;
13942
13946
  if (first.startsWith("claude:") && first.includes("aliased to")) {
13943
13947
  const aliasTarget = first.split("aliased to")[1]?.trim();
13944
- return aliasTarget && existsSync8(aliasTarget) ? aliasTarget : undefined;
13948
+ return aliasTarget && existsSync7(aliasTarget) ? aliasTarget : undefined;
13945
13949
  }
13946
13950
  return first;
13947
13951
  },
@@ -13958,7 +13962,7 @@ var init_binary = __esm(() => {
13958
13962
 
13959
13963
  // src/engine/codex-local/binary.ts
13960
13964
  import { spawnSync as spawnSync6 } from "child_process";
13961
- import { existsSync as existsSync9, statSync as statSync4 } from "fs";
13965
+ import { existsSync as existsSync8, statSync as statSync4 } from "fs";
13962
13966
  import { homedir as homedir13 } from "os";
13963
13967
  import path8 from "path";
13964
13968
  async function findCodexBinary(deps = defaultDeps5) {
@@ -14029,7 +14033,7 @@ var init_binary2 = __esm(() => {
14029
14033
  return;
14030
14034
  if (first.startsWith("codex:") && first.includes("aliased to")) {
14031
14035
  const aliasTarget = first.split("aliased to")[1]?.trim();
14032
- return aliasTarget && existsSync9(aliasTarget) ? aliasTarget : undefined;
14036
+ return aliasTarget && existsSync8(aliasTarget) ? aliasTarget : undefined;
14033
14037
  }
14034
14038
  return first;
14035
14039
  },
@@ -14046,7 +14050,7 @@ var init_binary2 = __esm(() => {
14046
14050
 
14047
14051
  // src/engine/copilot-local/binary.ts
14048
14052
  import { spawnSync as spawnSync7 } from "child_process";
14049
- import { existsSync as existsSync10, statSync as statSync5 } from "fs";
14053
+ import { existsSync as existsSync9, statSync as statSync5 } from "fs";
14050
14054
  import { homedir as homedir14 } from "os";
14051
14055
  import path9 from "path";
14052
14056
  async function findCopilotBinary(deps = defaultDeps6) {
@@ -14141,7 +14145,7 @@ var init_binary3 = __esm(() => {
14141
14145
  return;
14142
14146
  if (first.startsWith("copilot:") && first.includes("aliased to")) {
14143
14147
  const aliasTarget = first.split("aliased to")[1]?.trim();
14144
- return aliasTarget && existsSync10(aliasTarget) ? aliasTarget : undefined;
14148
+ return aliasTarget && existsSync9(aliasTarget) ? aliasTarget : undefined;
14145
14149
  }
14146
14150
  return first;
14147
14151
  },
@@ -14530,7 +14534,7 @@ var init_dialog_confirm = __esm(() => {
14530
14534
 
14531
14535
  // src/tui/component/settings-dialog/actions.ts
14532
14536
  import { unlinkSync as unlinkSync2 } from "fs";
14533
- import { join as join12 } from "path";
14537
+ import { join as join11 } from "path";
14534
14538
  function hasRestartableDaemon(orchestrator) {
14535
14539
  return orchestrator instanceof RemoteOrchestrator;
14536
14540
  }
@@ -14547,7 +14551,7 @@ async function confirmResetState(dialog, kv, renderer) {
14547
14551
  return;
14548
14552
  kv.clear();
14549
14553
  try {
14550
- unlinkSync2(join12(homeDir(), ".kobe", "tasks.json"));
14554
+ unlinkSync2(join11(homeDir(), ".kobe", "tasks.json"));
14551
14555
  } catch (err) {
14552
14556
  if (err.code !== "ENOENT") {
14553
14557
  console.error("kobe: failed to delete tasks.json during reset:", err);
@@ -16103,8 +16107,8 @@ var init_persisted_ui_prefs = __esm(() => {
16103
16107
 
16104
16108
  // src/tui/lib/worktree-opener.ts
16105
16109
  import { spawn as spawn3 } from "child_process";
16106
- import { existsSync as existsSync11 } from "fs";
16107
- import { basename as basename4, delimiter, isAbsolute, join as join13 } from "path";
16110
+ import { existsSync as existsSync10 } from "fs";
16111
+ import { basename as basename4, delimiter, isAbsolute, join as join12 } from "path";
16108
16112
  function executableOnPath(command, env, exists) {
16109
16113
  if (isAbsolute(command))
16110
16114
  return exists(command);
@@ -16112,7 +16116,7 @@ function executableOnPath(command, env, exists) {
16112
16116
  for (const dir of pathEnv.split(delimiter)) {
16113
16117
  if (!dir)
16114
16118
  continue;
16115
- if (exists(join13(dir, command)))
16119
+ if (exists(join12(dir, command)))
16116
16120
  return true;
16117
16121
  }
16118
16122
  return false;
@@ -16132,7 +16136,7 @@ function labelForOverride(command) {
16132
16136
  function detectWorktreeOpener(deps = {}) {
16133
16137
  const env = deps.env ?? process.env;
16134
16138
  const platform = deps.platform ?? process.platform;
16135
- const exists = deps.exists ?? existsSync11;
16139
+ const exists = deps.exists ?? existsSync10;
16136
16140
  const override = env.KOBE_OPEN_EDITOR?.trim();
16137
16141
  if (override) {
16138
16142
  return { id: "env", label: labelForOverride(override), command: override, args: [] };
@@ -17950,7 +17954,7 @@ var exports_host = {};
17950
17954
  __export(exports_host, {
17951
17955
  startTasksPane: () => startTasksPane
17952
17956
  });
17953
- import { existsSync as existsSync12 } from "fs";
17957
+ import { existsSync as existsSync11 } from "fs";
17954
17958
  import { TextAttributes as TextAttributes10 } from "@opentui/core";
17955
17959
  function TasksShell(props) {
17956
17960
  const themeCtx = useTheme();
@@ -18185,7 +18189,7 @@ function TasksShell(props) {
18185
18189
  async function openSelectedWorktree(id) {
18186
18190
  const task = props.tasks().find((t) => t.id === id);
18187
18191
  let worktree = task?.worktreePath;
18188
- if (!worktree || !existsSync12(worktree)) {
18192
+ if (!worktree || !existsSync11(worktree)) {
18189
18193
  if (!props.orch) {
18190
18194
  console.error("[kobe tasks] no daemon; cannot materialise worktree");
18191
18195
  return;
@@ -18198,7 +18202,7 @@ function TasksShell(props) {
18198
18202
  }
18199
18203
  await props.reload();
18200
18204
  }
18201
- if (!worktree || !existsSync12(worktree))
18205
+ if (!worktree || !existsSync11(worktree))
18202
18206
  return;
18203
18207
  const opener = detectWorktreeOpener();
18204
18208
  if (!opener) {
@@ -18249,7 +18253,7 @@ function TasksShell(props) {
18249
18253
  const exists = await sessionExists(name);
18250
18254
  if (exists) {
18251
18255
  const cwd2 = await getSessionOption(name, "@kobe_worktree") || task?.worktreePath || "";
18252
- if (cwd2 && existsSync12(cwd2)) {
18256
+ if (cwd2 && existsSync11(cwd2)) {
18253
18257
  await ensureSession({
18254
18258
  name,
18255
18259
  cwd: cwd2,
@@ -18263,7 +18267,7 @@ function TasksShell(props) {
18263
18267
  return;
18264
18268
  }
18265
18269
  let cwd = task?.worktreePath;
18266
- if (!cwd || !existsSync12(cwd)) {
18270
+ if (!cwd || !existsSync11(cwd)) {
18267
18271
  if (!props.orch) {
18268
18272
  console.error("[kobe tasks] no daemon; cannot materialise worktree");
18269
18273
  return;
@@ -18276,7 +18280,7 @@ function TasksShell(props) {
18276
18280
  }
18277
18281
  await props.reload();
18278
18282
  }
18279
- if (!cwd || !existsSync12(cwd))
18283
+ if (!cwd || !existsSync11(cwd))
18280
18284
  return;
18281
18285
  const init2 = task?.repo ? resolveRepoInit(task.repo, cwd) : {};
18282
18286
  const ready = await ensureSession({
@@ -19727,14 +19731,14 @@ var init_keys2 = __esm(() => {
19727
19731
 
19728
19732
  // src/tui/panes/filetree/open-external.ts
19729
19733
  import { spawn as spawn5 } from "child_process";
19730
- import { existsSync as existsSync13 } from "fs";
19734
+ import { existsSync as existsSync12 } from "fs";
19731
19735
  import { platform } from "os";
19732
19736
  function openExternally(absPath) {
19733
19737
  if (!absPath)
19734
19738
  return;
19735
19739
  const plat = platform();
19736
19740
  if (plat === "linux") {
19737
- if (existsSync13("/proc/sys/fs/binfmt_misc/WSLInterop") || process.env.WSL_DISTRO_NAME) {
19741
+ if (existsSync12("/proc/sys/fs/binfmt_misc/WSLInterop") || process.env.WSL_DISTRO_NAME) {
19738
19742
  spawnDetached("wslview", [absPath], () => {
19739
19743
  const child = spawn5("wslpath", ["-w", absPath], { stdio: ["ignore", "pipe", "ignore"] });
19740
19744
  let out = "";
@@ -21175,7 +21179,7 @@ async function ensureRepos(orchestrator) {
21175
21179
  }
21176
21180
  async function startDirectTmux() {
21177
21181
  setClientLogContext("gui");
21178
- Promise.resolve().then(() => (init_hook_cmd(), exports_hook_cmd)).then((m) => m.ensureDefaultWorktreeSync());
21182
+ Promise.resolve().then(() => (init_hook_cmd(), exports_hook_cmd)).then((m) => m.ensureGlobalKobeHooks());
21179
21183
  if (!await tmuxAvailable()) {
21180
21184
  console.error("kobe: tmux not found on PATH \u2014 install tmux to use kobe 0.6 direct mode");
21181
21185
  process.exitCode = 1;
@@ -21769,9 +21773,9 @@ var pulse_default = "../pulse-n3cq1btw.wav";
21769
21773
  var init_pulse = () => {};
21770
21774
 
21771
21775
  // src/tui/lib/sound.ts
21772
- import { existsSync as existsSync14, mkdirSync as mkdirSync5 } from "fs";
21776
+ import { existsSync as existsSync13, mkdirSync as mkdirSync5 } from "fs";
21773
21777
  import { tmpdir as tmpdir2 } from "os";
21774
- import { basename as basename6, isAbsolute as isAbsolute2, join as join14, resolve as resolve8 } from "path";
21778
+ import { basename as basename6, isAbsolute as isAbsolute2, join as join13, resolve as resolve8 } from "path";
21775
21779
  function args(player, file, volume) {
21776
21780
  if (player === "ffplay")
21777
21781
  return [player, "-autoexit", "-nodisp", "-af", `volume=${volume}`, file];
@@ -21794,13 +21798,13 @@ function pickPlayer() {
21794
21798
  return cachedPlayer;
21795
21799
  const path12 = process.env.PATH ?? "";
21796
21800
  const segments = path12.split(":").filter(Boolean);
21797
- cachedPlayer = PLAYERS.find((p) => segments.some((dir) => existsSync14(join14(dir, p)))) ?? null;
21801
+ cachedPlayer = PLAYERS.find((p) => segments.some((dir) => existsSync13(join13(dir, p)))) ?? null;
21798
21802
  return cachedPlayer;
21799
21803
  }
21800
21804
  async function ensureAsset() {
21801
21805
  cachedPath ??= (async () => {
21802
21806
  mkdirSync5(DIR, { recursive: true });
21803
- const dest = join14(DIR, basename6(pulseAsset));
21807
+ const dest = join13(DIR, basename6(pulseAsset));
21804
21808
  const out = Bun.file(dest);
21805
21809
  if (await out.exists())
21806
21810
  return dest;
@@ -21830,7 +21834,7 @@ var pulseAsset, DIR, PLAYERS, cachedPlayer, cachedPath;
21830
21834
  var init_sound = __esm(() => {
21831
21835
  init_pulse();
21832
21836
  pulseAsset = isAbsolute2(pulse_default) ? pulse_default : resolve8(import.meta.dir, pulse_default);
21833
- DIR = join14(tmpdir2(), "kobe-sfx");
21837
+ DIR = join13(tmpdir2(), "kobe-sfx");
21834
21838
  PLAYERS = [
21835
21839
  "ffplay",
21836
21840
  "mpv",
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.8",
4
+ "version": "0.7.9",
5
5
  "description": "TUI orchestrator for Claude Code (codename)",
6
6
  "type": "module",
7
7
  "packageManager": "bun@1.3.13",