@towles/tool 0.0.103 → 0.0.104

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/README.md CHANGED
@@ -17,8 +17,8 @@ claude plugin update tt@towles-tool
17
17
  ```bash
18
18
  git clone https://github.com/ChrisTowles/towles-tool.git
19
19
  cd towles-tool
20
- pnpm install
21
- pnpm start
20
+ bun install
21
+ bun start
22
22
  ```
23
23
 
24
24
  ## CLI Commands
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@towles/tool",
3
- "version": "0.0.103",
3
+ "version": "0.0.104",
4
4
  "description": "One off quality of life scripts that I use on a daily basis.",
5
5
  "homepage": "https://github.com/ChrisTowles/towles-tool#readme",
6
6
  "bugs": {
@@ -39,6 +39,8 @@
39
39
  "test": "vitest run",
40
40
  "test:watch": "CI=DisableCallingClaude vitest watch",
41
41
  "typecheck": "tsgo --noEmit --incremental",
42
+ "link": "bun link",
43
+ "link:show": "readlink -f $(which towles-tool)",
42
44
  "prepare": "simple-git-hooks"
43
45
  },
44
46
  "dependencies": {
@@ -14,12 +14,11 @@ const PLUGIN_DIR = resolve(import.meta.dirname, "../../plugins/tt-agentboard");
14
14
  // Keybinding defaults
15
15
  const DEFAULT_KEY = "a";
16
16
  const TMUX_BINDINGS = { toggle: "t", focus: "s" } as const;
17
- const RUN_SHELL_LINE = `run-shell '${PLUGIN_DIR}/agentboard.tmux'`;
17
+ const RUN_SHELL_LINE = "run-shell 'tt agentboard init'";
18
18
  const MARKER = "# agentboard";
19
19
 
20
20
  function findTmuxConf(): string | null {
21
21
  const candidates = [
22
- resolve(process.env.HOME ?? "~", ".tmux.conf"),
23
22
  resolve(process.env.HOME ?? "~", ".config/tmux/tmux.conf"),
24
23
  ];
25
24
  for (const path of candidates) {
@@ -123,7 +122,7 @@ function setup(): void {
123
122
  }
124
123
 
125
124
  const content = readFileSync(editPath, "utf8");
126
- if (content.includes("agentboard.tmux")) {
125
+ if (content.includes(MARKER)) {
127
126
  consola.success("Already installed in tmux.conf");
128
127
  reloadTmux();
129
128
  return;
@@ -227,6 +226,122 @@ async function ensureServerUp(): Promise<boolean> {
227
226
  return false;
228
227
  }
229
228
 
229
+ function tmuxDisplay(fmt: string): string {
230
+ try {
231
+ const r = spawnSync("tmux", ["display-message", "-p", fmt], {
232
+ encoding: "utf8",
233
+ stdio: ["pipe", "pipe", "pipe"],
234
+ });
235
+ return (r.stdout ?? "").trim();
236
+ } catch {
237
+ return "";
238
+ }
239
+ }
240
+
241
+ function tmuxContext(): string {
242
+ return tmuxDisplay("#{client_tty}|#{session_name}|#{window_id}");
243
+ }
244
+
245
+ function resetTmuxKeys(): void {
246
+ spawnSync("tmux", ["switch-client", "-T", "root"], { stdio: "pipe" });
247
+ }
248
+
249
+ function findSidebarPane(windowId: string): string | null {
250
+ try {
251
+ const r = spawnSync("tmux", ["list-panes", "-t", windowId, "-F", "#{pane_id} #{pane_title}"], {
252
+ encoding: "utf8",
253
+ stdio: ["pipe", "pipe", "pipe"],
254
+ });
255
+ for (const line of (r.stdout ?? "").trim().split("\n")) {
256
+ const [paneId, title] = line.split(" ", 2);
257
+ if (title === "agentboard-sidebar" && paneId) return paneId;
258
+ }
259
+ } catch {}
260
+ return null;
261
+ }
262
+
263
+ function tmux(...args: string[]): void {
264
+ spawnSync("tmux", args, { stdio: "pipe" });
265
+ }
266
+
267
+ function init(): void {
268
+ const port = process.env.TT_AGENTBOARD_PORT ?? "4201";
269
+ const host = process.env.TT_AGENTBOARD_HOST ?? "127.0.0.1";
270
+
271
+ // Read tmux options with defaults
272
+ const keyResult = spawnSync("tmux", ["show-option", "-gqv", "@agentboard-key"], {
273
+ encoding: "utf8",
274
+ stdio: ["pipe", "pipe", "pipe"],
275
+ });
276
+ const key = (keyResult.stdout ?? "").trim() || DEFAULT_KEY;
277
+
278
+ // Export to tmux environment
279
+ tmux("set-environment", "-g", "TT_AGENTBOARD_PORT", port);
280
+ tmux("set-environment", "-g", "TT_AGENTBOARD_HOST", host);
281
+
282
+ // Bind keybindings via command table "agentboard"
283
+ tmux("bind-key", "-T", "prefix", key, "switch-client", "-T", "agentboard");
284
+ tmux("bind-key", "-T", "agentboard", TMUX_BINDINGS.toggle, "run-shell", "tt agentboard run --toggle");
285
+ tmux("bind-key", "-T", "agentboard", TMUX_BINDINGS.focus, "run-shell", "tt agentboard run --focus");
286
+
287
+ // Number keys 1-9 switch to session by index
288
+ for (let i = 1; i <= 9; i++) {
289
+ tmux(
290
+ "bind-key", "-T", "agentboard", String(i), "run-shell",
291
+ `curl -s -X POST 'http://${host}:${port}/switch-index?index=${i}' -d "$(tmux display-message -p '#{q:client_tty}|#{q:session_name}|#{q:window_id}')" >/dev/null 2>&1 || true`,
292
+ );
293
+ }
294
+
295
+ // Hooks (fallback for when server isn't running yet)
296
+ const hookPost = (path: string, body?: string) => {
297
+ const bodyArg = body ? ` -d \\"${body}\\"` : "";
298
+ return `run-shell -b "curl -s -X POST http://${host}:${port}${path}${bodyArg} >/dev/null 2>&1 || true"`;
299
+ };
300
+ const focusBody = "#{q:client_tty}|#{q:session_name}|#{q:window_id}";
301
+ const resizeBody = "#{q:pane_id}|#{q:session_name}|#{q:window_id}|#{q:pane_width}|#{q:window_width}";
302
+
303
+ tmux("set-hook", "-g", "client-session-changed", hookPost("/focus", focusBody));
304
+ tmux("set-hook", "-g", "after-select-window", hookPost("/ensure-sidebar", focusBody));
305
+ tmux("set-hook", "-g", "after-resize-pane", hookPost("/resize-sidebars", resizeBody));
306
+ }
307
+
308
+ async function runToggle(): Promise<void> {
309
+ if (!(await ensureServerUp())) process.exit(0);
310
+ const ctx = tmuxContext();
311
+ await fetch(`http://${SERVER_HOST}:${SERVER_PORT}/toggle`, { method: "POST", body: ctx }).catch(() => {});
312
+ resetTmuxKeys();
313
+ }
314
+
315
+ async function runFocus(): Promise<void> {
316
+ const windowId = tmuxDisplay("#{window_id}");
317
+ if (!windowId) process.exit(0);
318
+
319
+ // If sidebar already exists, just focus it
320
+ const existing = findSidebarPane(windowId);
321
+ if (existing) {
322
+ spawnSync("tmux", ["select-pane", "-t", existing], { stdio: "pipe" });
323
+ resetTmuxKeys();
324
+ return;
325
+ }
326
+
327
+ // Otherwise, ensure server + toggle sidebar on
328
+ if (!(await ensureServerUp())) process.exit(0);
329
+ const ctx = tmuxContext();
330
+ await fetch(`http://${SERVER_HOST}:${SERVER_PORT}/toggle`, { method: "POST", body: ctx }).catch(() => {});
331
+
332
+ // Wait for sidebar pane to appear
333
+ for (let i = 0; i < 20; i++) {
334
+ const paneId = findSidebarPane(windowId);
335
+ if (paneId) {
336
+ spawnSync("tmux", ["select-pane", "-t", paneId], { stdio: "pipe" });
337
+ resetTmuxKeys();
338
+ return;
339
+ }
340
+ await new Promise((r) => setTimeout(r, 50));
341
+ }
342
+ resetTmuxKeys();
343
+ }
344
+
230
345
  async function restart(): Promise<void> {
231
346
  ensureDeps();
232
347
 
@@ -293,8 +408,10 @@ export default defineCommand({
293
408
  subcommand: {
294
409
  type: "positional",
295
410
  required: false,
296
- description: "Subcommand: setup, uninstall, server, tui, start, restart, keys",
411
+ description: "Subcommand: setup, uninstall, server, tui, start, restart, run, keys",
297
412
  },
413
+ toggle: { type: "boolean", description: "Toggle sidebar (used with 'run')" },
414
+ focus: { type: "boolean", description: "Focus sidebar (used with 'run')" },
298
415
  },
299
416
  async run({ args }) {
300
417
  switch (args.subcommand) {
@@ -316,6 +433,14 @@ export default defineCommand({
316
433
  case "restart":
317
434
  await restart();
318
435
  break;
436
+ case "init":
437
+ init();
438
+ break;
439
+ case "run":
440
+ if (args.toggle) await runToggle();
441
+ else if (args.focus) await runFocus();
442
+ else consola.error("Usage: tt agentboard run --toggle | --focus");
443
+ break;
319
444
  case "keys":
320
445
  showKeys();
321
446
  break;