@generativereality/agentherder 0.1.2 → 0.1.4

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/index.js +90 -29
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import { consola } from "consola";
10
10
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
11
11
  //#region package.json
12
12
  var name = "@generativereality/agentherder";
13
- var version = "0.1.2";
13
+ var version = "0.1.4";
14
14
  var description = "Agent session manager for AI coding tools. Terminal tabs as the UI, no tmux.";
15
15
  var package_default = {
16
16
  name,
@@ -534,6 +534,7 @@ async function openSession(opts) {
534
534
  const extraFlags = config.claude.flags.join(" ");
535
535
  const cmd = `cd ${JSON.stringify(dir)} && ${claudeCmd} --name ${JSON.stringify(tabName)}${extraFlags ? " " + extraFlags : ""}\n`;
536
536
  await adapter.sendInput(blockId, cmd);
537
+ await new Promise((r) => setTimeout(r, 2e3));
537
538
  adapter.closeSocket();
538
539
  return tabId;
539
540
  }
@@ -617,20 +618,63 @@ const resumeCommand = define({
617
618
  function pathToProjectSlug(dir) {
618
619
  return resolve(dir).replace(/\//g, "-");
619
620
  }
620
- /** Find the most recent Claude Code session ID for a directory */
621
- function findLatestSessionId(dir) {
622
- const slug = pathToProjectSlug(dir);
623
- const projectDir = join(homedir(), ".claude", "projects", slug);
621
+ /** Find the most recent .jsonl session file in a Claude project directory */
622
+ function latestJsonlIn(projectDir) {
624
623
  if (!existsSync(projectDir)) return null;
625
- const jsonlFiles = readdirSync(projectDir).filter((f) => extname(f) === ".jsonl").map((f) => ({
624
+ const files = readdirSync(projectDir).filter((f) => extname(f) === ".jsonl").map((f) => ({
626
625
  name: f,
627
626
  mtime: statSync(join(projectDir, f)).mtimeMs
628
627
  })).sort((a, b) => b.mtime - a.mtime);
629
- if (!jsonlFiles.length) return null;
630
- return basename(jsonlFiles[0].name, ".jsonl");
628
+ return files.length ? basename(files[0].name, ".jsonl") : null;
629
+ }
630
+ /**
631
+ * Find the most recent Claude Code session ID for a directory.
632
+ * Also checks worktree subdirectories (.claude/worktrees/*) since tabs
633
+ * opened with --worktree run from a worktree path, not the repo root.
634
+ */
635
+ function findLatestSessionId(dir) {
636
+ const projectsRoot = join(homedir(), ".claude", "projects");
637
+ const direct = latestJsonlIn(join(projectsRoot, pathToProjectSlug(dir)));
638
+ if (direct) return direct;
639
+ const worktreesDir = join(dir, ".claude", "worktrees");
640
+ if (existsSync(worktreesDir)) {
641
+ const candidates = [];
642
+ for (const entry of readdirSync(worktreesDir)) {
643
+ const projectDir = join(projectsRoot, pathToProjectSlug(join(worktreesDir, entry)));
644
+ const id = latestJsonlIn(projectDir);
645
+ if (id) {
646
+ const mtime = statSync(join(projectDir, id + ".jsonl")).mtimeMs;
647
+ candidates.push({
648
+ id,
649
+ mtime
650
+ });
651
+ }
652
+ }
653
+ if (candidates.length) {
654
+ candidates.sort((a, b) => b.mtime - a.mtime);
655
+ return candidates[0].id;
656
+ }
657
+ }
658
+ return null;
631
659
  }
632
660
  //#endregion
633
661
  //#region src/commands/fork.ts
662
+ /** If dir is inside .claude/worktrees/<name>, return the repo root instead */
663
+ function resolveSessionDir(dir) {
664
+ const worktreeMarker = `${join(".claude", "worktrees")}/`;
665
+ const idx = dir.indexOf(worktreeMarker);
666
+ if (idx !== -1) {
667
+ const repoRoot = dir.slice(0, idx - 1);
668
+ return {
669
+ sessionLookupDir: repoRoot,
670
+ openDir: repoRoot
671
+ };
672
+ }
673
+ return {
674
+ sessionLookupDir: dir,
675
+ openDir: dir
676
+ };
677
+ }
634
678
  const forkCommand = define({
635
679
  name: "fork",
636
680
  description: "Fork a session into a new tab (claude --resume <id> --fork-session)",
@@ -672,16 +716,16 @@ const forkCommand = define({
672
716
  consola.error(`Tab "${tabName}" has no terminal block`);
673
717
  process.exit(1);
674
718
  }
675
- const sourceDir = termBlocks[0].meta?.["cmd:cwd"] ?? process.cwd();
676
- const sessionId = findLatestSessionId(sourceDir);
719
+ const { sessionLookupDir, openDir } = resolveSessionDir(termBlocks[0].meta?.["cmd:cwd"] ?? process.cwd());
720
+ const sessionId = findLatestSessionId(sessionLookupDir);
677
721
  if (!sessionId) {
678
- consola.error(`No Claude session found for ${sourceDir}`);
679
- consola.info(`Looked in ~/.claude/projects/${pathToProjectSlug(sourceDir)}/`);
722
+ consola.error(`No Claude session found for ${sessionLookupDir}`);
723
+ consola.info(`Looked in ~/.claude/projects/${pathToProjectSlug(sessionLookupDir)}/`);
680
724
  process.exit(1);
681
725
  }
682
726
  const newTabId = await openSession({
683
727
  tabName: newName,
684
- dir: sourceDir,
728
+ dir: openDir,
685
729
  claudeCmd: `claude --resume ${sessionId} --fork-session`
686
730
  });
687
731
  consola.success(`Forked "${tabName}" → "${newName}" [${newTabId.slice(0, 8)}]`);
@@ -707,7 +751,7 @@ const closeCommand = define({
707
751
  const { tabsById, tabNames } = await adapter.getAllData();
708
752
  const matches = adapter.resolveTab(query, tabsById, tabNames);
709
753
  if (!matches.length) {
710
- consola.error(`No tab matching '${query}'`);
754
+ consola.error(`No tab matching '${query}' (tabs in workspaces with no open window are not visible — open that workspace first)`);
711
755
  process.exit(1);
712
756
  }
713
757
  if (matches.length > 1) {
@@ -766,11 +810,11 @@ const renameCommand = define({
766
810
  //#region src/commands/scrollback.ts
767
811
  const scrollbackCommand = define({
768
812
  name: "scrollback",
769
- description: "Show terminal output for a block (default: last 50 lines)",
813
+ description: "Show terminal output for a tab or block (default: last 50 lines)",
770
814
  args: {
771
- block: {
815
+ target: {
772
816
  type: "positional",
773
- description: "Block ID prefix"
817
+ description: "Tab name, tab ID prefix, or block ID prefix"
774
818
  },
775
819
  lines: {
776
820
  type: "number",
@@ -782,22 +826,39 @@ const scrollbackCommand = define({
782
826
  const query = ctx.positionals[1];
783
827
  const lines = ctx.values.lines ?? 50;
784
828
  if (!query) {
785
- consola.error("Block ID is required");
829
+ consola.error("Tab name or block ID is required");
786
830
  process.exit(1);
787
831
  }
788
832
  const adapter = requireWaveAdapter();
789
- const blocks = adapter.blocksList();
790
- const matches = adapter.resolveBlock(query, blocks);
791
- if (!matches.length) {
792
- consola.error(`No block matching '${query}'`);
793
- process.exit(1);
794
- }
795
- if (matches.length > 1) {
796
- consola.error(`Multiple blocks match '${query}':`);
797
- for (const b of matches) consola.log(` ${b.blockid}`);
833
+ const { tabsById, tabNames } = await adapter.getAllData();
834
+ const tabMatches = adapter.resolveTab(query, tabsById, tabNames);
835
+ let blockId;
836
+ if (tabMatches.length === 1) {
837
+ const blocks = (tabsById.get(tabMatches[0]) ?? []).filter((b) => b.view === "term");
838
+ if (!blocks.length) {
839
+ consola.error(`Tab "${tabNames.get(tabMatches[0])}" has no terminal block`);
840
+ process.exit(1);
841
+ }
842
+ blockId = blocks[0].blockid;
843
+ } else if (tabMatches.length > 1) {
844
+ consola.error(`Multiple tabs match '${query}':`);
845
+ for (const tid of tabMatches) consola.log(` "${tabNames.get(tid)}" [${tid.slice(0, 8)}]`);
798
846
  process.exit(1);
847
+ } else {
848
+ const allBlocks = adapter.blocksList();
849
+ const blockMatches = adapter.resolveBlock(query, allBlocks);
850
+ if (!blockMatches.length) {
851
+ consola.error(`No tab or block matching '${query}' (tabs in workspaces with no open window are not visible — open that workspace first)`);
852
+ process.exit(1);
853
+ }
854
+ if (blockMatches.length > 1) {
855
+ consola.error(`Multiple blocks match '${query}':`);
856
+ for (const b of blockMatches) consola.log(` ${b.blockid}`);
857
+ process.exit(1);
858
+ }
859
+ blockId = blockMatches[0].blockid;
799
860
  }
800
- process.stdout.write(adapter.scrollback(matches[0].blockid, lines));
861
+ process.stdout.write(adapter.scrollback(blockId, lines));
801
862
  }
802
863
  });
803
864
  //#endregion
@@ -861,7 +922,7 @@ const sendCommand = define({
861
922
  const allBlocks = adapter.blocksList();
862
923
  const blockMatches = adapter.resolveBlock(query, allBlocks);
863
924
  if (!blockMatches.length) {
864
- consola.error(`No tab or block matching '${query}'`);
925
+ consola.error(`No tab or block matching '${query}' (tabs in workspaces with no open window are not visible — open that workspace first)`);
865
926
  process.exit(1);
866
927
  }
867
928
  if (blockMatches.length > 1) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@generativereality/agentherder",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Agent session manager for AI coding tools. Terminal tabs as the UI, no tmux.",
5
5
  "type": "module",
6
6
  "bin": {