@towles/tool 0.0.103 → 0.0.105

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.105",
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,14 +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
- const candidates = [
22
- resolve(process.env.HOME ?? "~", ".tmux.conf"),
23
- resolve(process.env.HOME ?? "~", ".config/tmux/tmux.conf"),
24
- ];
21
+ const candidates = [resolve(process.env.HOME ?? "~", ".config/tmux/tmux.conf")];
25
22
  for (const path of candidates) {
26
23
  try {
27
24
  const real = existsSync(path) ? path : null;
@@ -123,7 +120,7 @@ function setup(): void {
123
120
  }
124
121
 
125
122
  const content = readFileSync(editPath, "utf8");
126
- if (content.includes("agentboard.tmux")) {
123
+ if (content.includes(MARKER)) {
127
124
  consola.success("Already installed in tmux.conf");
128
125
  reloadTmux();
129
126
  return;
@@ -227,6 +224,145 @@ async function ensureServerUp(): Promise<boolean> {
227
224
  return false;
228
225
  }
229
226
 
227
+ function tmuxDisplay(fmt: string): string {
228
+ try {
229
+ const r = spawnSync("tmux", ["display-message", "-p", fmt], {
230
+ encoding: "utf8",
231
+ stdio: ["pipe", "pipe", "pipe"],
232
+ });
233
+ return (r.stdout ?? "").trim();
234
+ } catch {
235
+ return "";
236
+ }
237
+ }
238
+
239
+ function tmuxContext(): string {
240
+ return tmuxDisplay("#{client_tty}|#{session_name}|#{window_id}");
241
+ }
242
+
243
+ function resetTmuxKeys(): void {
244
+ spawnSync("tmux", ["switch-client", "-T", "root"], { stdio: "pipe" });
245
+ }
246
+
247
+ function findSidebarPane(windowId: string): string | null {
248
+ try {
249
+ const r = spawnSync("tmux", ["list-panes", "-t", windowId, "-F", "#{pane_id} #{pane_title}"], {
250
+ encoding: "utf8",
251
+ stdio: ["pipe", "pipe", "pipe"],
252
+ });
253
+ for (const line of (r.stdout ?? "").trim().split("\n")) {
254
+ const [paneId, title] = line.split(" ", 2);
255
+ if (title === "agentboard-sidebar" && paneId) return paneId;
256
+ }
257
+ } catch {}
258
+ return null;
259
+ }
260
+
261
+ function tmux(...args: string[]): void {
262
+ spawnSync("tmux", args, { stdio: "pipe" });
263
+ }
264
+
265
+ function init(): void {
266
+ const port = process.env.TT_AGENTBOARD_PORT ?? "4201";
267
+ const host = process.env.TT_AGENTBOARD_HOST ?? "127.0.0.1";
268
+
269
+ // Read tmux options with defaults
270
+ const keyResult = spawnSync("tmux", ["show-option", "-gqv", "@agentboard-key"], {
271
+ encoding: "utf8",
272
+ stdio: ["pipe", "pipe", "pipe"],
273
+ });
274
+ const key = (keyResult.stdout ?? "").trim() || DEFAULT_KEY;
275
+
276
+ // Export to tmux environment
277
+ tmux("set-environment", "-g", "TT_AGENTBOARD_PORT", port);
278
+ tmux("set-environment", "-g", "TT_AGENTBOARD_HOST", host);
279
+
280
+ // Bind keybindings via command table "agentboard"
281
+ tmux("bind-key", "-T", "prefix", key, "switch-client", "-T", "agentboard");
282
+ tmux(
283
+ "bind-key",
284
+ "-T",
285
+ "agentboard",
286
+ TMUX_BINDINGS.toggle,
287
+ "run-shell",
288
+ "tt agentboard run --toggle",
289
+ );
290
+ tmux(
291
+ "bind-key",
292
+ "-T",
293
+ "agentboard",
294
+ TMUX_BINDINGS.focus,
295
+ "run-shell",
296
+ "tt agentboard run --focus",
297
+ );
298
+
299
+ // Number keys 1-9 switch to session by index
300
+ for (let i = 1; i <= 9; i++) {
301
+ tmux(
302
+ "bind-key",
303
+ "-T",
304
+ "agentboard",
305
+ String(i),
306
+ "run-shell",
307
+ `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`,
308
+ );
309
+ }
310
+
311
+ // Hooks (fallback for when server isn't running yet)
312
+ const hookPost = (path: string, body?: string) => {
313
+ const bodyArg = body ? ` -d \\"${body}\\"` : "";
314
+ return `run-shell -b "curl -s -X POST http://${host}:${port}${path}${bodyArg} >/dev/null 2>&1 || true"`;
315
+ };
316
+ const focusBody = "#{q:client_tty}|#{q:session_name}|#{q:window_id}";
317
+ const resizeBody =
318
+ "#{q:pane_id}|#{q:session_name}|#{q:window_id}|#{q:pane_width}|#{q:window_width}";
319
+
320
+ tmux("set-hook", "-g", "client-session-changed", hookPost("/focus", focusBody));
321
+ tmux("set-hook", "-g", "after-select-window", hookPost("/ensure-sidebar", focusBody));
322
+ tmux("set-hook", "-g", "after-resize-pane", hookPost("/resize-sidebars", resizeBody));
323
+ }
324
+
325
+ async function runToggle(): Promise<void> {
326
+ if (!(await ensureServerUp())) process.exit(0);
327
+ const ctx = tmuxContext();
328
+ await fetch(`http://${SERVER_HOST}:${SERVER_PORT}/toggle`, { method: "POST", body: ctx }).catch(
329
+ () => {},
330
+ );
331
+ resetTmuxKeys();
332
+ }
333
+
334
+ async function runFocus(): Promise<void> {
335
+ const windowId = tmuxDisplay("#{window_id}");
336
+ if (!windowId) process.exit(0);
337
+
338
+ // If sidebar already exists, just focus it
339
+ const existing = findSidebarPane(windowId);
340
+ if (existing) {
341
+ spawnSync("tmux", ["select-pane", "-t", existing], { stdio: "pipe" });
342
+ resetTmuxKeys();
343
+ return;
344
+ }
345
+
346
+ // Otherwise, ensure server + toggle sidebar on
347
+ if (!(await ensureServerUp())) process.exit(0);
348
+ const ctx = tmuxContext();
349
+ await fetch(`http://${SERVER_HOST}:${SERVER_PORT}/toggle`, { method: "POST", body: ctx }).catch(
350
+ () => {},
351
+ );
352
+
353
+ // Wait for sidebar pane to appear
354
+ for (let i = 0; i < 20; i++) {
355
+ const paneId = findSidebarPane(windowId);
356
+ if (paneId) {
357
+ spawnSync("tmux", ["select-pane", "-t", paneId], { stdio: "pipe" });
358
+ resetTmuxKeys();
359
+ return;
360
+ }
361
+ await new Promise((r) => setTimeout(r, 50));
362
+ }
363
+ resetTmuxKeys();
364
+ }
365
+
230
366
  async function restart(): Promise<void> {
231
367
  ensureDeps();
232
368
 
@@ -293,8 +429,10 @@ export default defineCommand({
293
429
  subcommand: {
294
430
  type: "positional",
295
431
  required: false,
296
- description: "Subcommand: setup, uninstall, server, tui, start, restart, keys",
432
+ description: "Subcommand: setup, uninstall, server, tui, start, restart, run, keys",
297
433
  },
434
+ toggle: { type: "boolean", description: "Toggle sidebar (used with 'run')" },
435
+ focus: { type: "boolean", description: "Focus sidebar (used with 'run')" },
298
436
  },
299
437
  async run({ args }) {
300
438
  switch (args.subcommand) {
@@ -316,6 +454,14 @@ export default defineCommand({
316
454
  case "restart":
317
455
  await restart();
318
456
  break;
457
+ case "init":
458
+ init();
459
+ break;
460
+ case "run":
461
+ if (args.toggle) await runToggle();
462
+ else if (args.focus) await runFocus();
463
+ else consola.error("Usage: tt agentboard run --toggle | --focus");
464
+ break;
319
465
  case "keys":
320
466
  showKeys();
321
467
  break;