@towles/tool 0.0.109 → 0.0.111

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 (190) hide show
  1. package/package.json +9 -4
  2. package/{plugins/tt-agentboard → packages/agentboard}/README.md +1 -1
  3. package/{plugins/tt-agentboard → packages/agentboard}/apps/server/package.json +2 -1
  4. package/{plugins/tt-agentboard → packages/agentboard}/apps/server/src/main.ts +6 -20
  5. package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/package.json +4 -0
  6. package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/DetailPanel.tsx +3 -2
  7. package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/StatusBar.tsx +35 -0
  8. package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/constants.ts +1 -0
  9. package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/index.tsx +206 -226
  10. package/packages/agentboard/apps/tui/src/session-status.test.ts +70 -0
  11. package/packages/agentboard/apps/tui/src/session-status.ts +19 -0
  12. package/{plugins/tt-agentboard → packages/agentboard}/package.json +2 -6
  13. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/package.json +3 -0
  14. package/{plugins/tt-agentboard/packages/runtime/test → packages/agentboard/packages/runtime/src/agents}/tracker.test.ts +2 -2
  15. package/packages/agentboard/packages/runtime/src/agents/watchers/claude-code.test.ts +63 -0
  16. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/claude-code.ts +26 -2
  17. package/packages/agentboard/packages/runtime/src/config.test.ts +107 -0
  18. package/packages/agentboard/packages/runtime/src/config.ts +80 -0
  19. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/index.ts +1 -1
  20. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/plugins/loader.ts +1 -33
  21. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/git-info.ts +3 -2
  22. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/index.ts +23 -37
  23. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/launcher.ts +6 -18
  24. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/pane-scanner.ts +6 -0
  25. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/shared.ts +7 -2
  26. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/tsconfig.json +1 -1
  27. package/packages/shared/package.json +15 -0
  28. package/packages/shared/src/git/exec.ts +41 -0
  29. package/{src/utils → packages/shared/src}/git/gh-cli-wrapper.ts +13 -18
  30. package/packages/shared/src/index.ts +8 -0
  31. package/packages/shared/tsconfig.json +16 -0
  32. package/src/cli.ts +1 -1
  33. package/src/commands/agentboard.ts +51 -67
  34. package/src/{lib → commands}/auto-claude/claude-cli.ts +1 -1
  35. package/src/commands/auto-claude/config-init-helpers.ts +79 -0
  36. package/src/commands/auto-claude/config-init.test.ts +137 -0
  37. package/src/commands/auto-claude/config-init.ts +159 -0
  38. package/src/{lib → commands}/auto-claude/config.ts +4 -8
  39. package/src/{lib → commands}/auto-claude/e2e.test.ts +6 -6
  40. package/src/commands/auto-claude/explain.test.ts +58 -0
  41. package/src/commands/auto-claude/explain.ts +97 -0
  42. package/src/commands/auto-claude/index.ts +37 -14
  43. package/src/{lib → commands}/auto-claude/labels.ts +1 -1
  44. package/src/commands/auto-claude/list.ts +5 -4
  45. package/src/{lib → commands}/auto-claude/pipeline-execution.test.ts +1 -1
  46. package/src/{lib → commands}/auto-claude/pipeline.ts +1 -3
  47. package/src/commands/auto-claude/retry.test.ts +2 -2
  48. package/src/commands/auto-claude/retry.ts +5 -5
  49. package/src/commands/auto-claude/shell.ts +3 -0
  50. package/src/commands/auto-claude/status.test.ts +2 -2
  51. package/src/commands/auto-claude/status.ts +4 -4
  52. package/src/{lib → commands}/auto-claude/steps/create-pr.ts +1 -3
  53. package/src/{lib → commands}/auto-claude/steps/fetch-issues.ts +1 -1
  54. package/src/{lib → commands}/auto-claude/steps/implement.ts +1 -2
  55. package/src/{lib → commands}/auto-claude/utils-execution.test.ts +6 -6
  56. package/src/{lib → commands}/auto-claude/utils.ts +10 -4
  57. package/src/{lib/install → commands}/claude-settings.ts +1 -1
  58. package/src/commands/config/config.test.ts +129 -0
  59. package/src/commands/config/index.ts +11 -0
  60. package/src/commands/config/reset.ts +53 -0
  61. package/src/commands/config/schema.ts +19 -0
  62. package/src/commands/{config.ts → config/show.ts} +2 -2
  63. package/src/commands/config/validate.ts +51 -0
  64. package/src/commands/doctor/checks.ts +167 -0
  65. package/src/commands/doctor/format.test.ts +63 -0
  66. package/src/commands/doctor/format.ts +5 -0
  67. package/src/commands/doctor/history.test.ts +161 -0
  68. package/src/commands/doctor/history.ts +130 -0
  69. package/src/commands/doctor.ts +80 -151
  70. package/src/commands/gh/branch-clean.ts +4 -4
  71. package/src/commands/gh/branch.test.ts +4 -5
  72. package/src/commands/gh/branch.ts +10 -5
  73. package/src/commands/gh/pr.ts +6 -7
  74. package/src/{lib → commands}/graph/analyzer.test.ts +4 -4
  75. package/src/commands/graph/format.test.ts +130 -0
  76. package/src/commands/graph/format.ts +94 -0
  77. package/src/commands/graph/index.ts +69 -41
  78. package/src/{lib → commands}/graph/labels.ts +4 -4
  79. package/src/{lib → commands}/graph/server.ts +2 -2
  80. package/src/{lib → commands}/graph/types.ts +2 -0
  81. package/src/commands/graph.test.ts +1 -1
  82. package/src/commands/install.ts +6 -6
  83. package/src/commands/journal/daily-notes.ts +4 -7
  84. package/src/{lib → commands}/journal/fs.ts +1 -1
  85. package/src/commands/journal/index.ts +2 -0
  86. package/src/commands/journal/list.test.ts +174 -0
  87. package/src/commands/journal/list.ts +213 -0
  88. package/src/commands/journal/meeting.ts +4 -7
  89. package/src/commands/journal/note.ts +4 -7
  90. package/src/{lib → commands}/journal/paths.ts +1 -1
  91. package/src/commands/journal/search.test.ts +156 -0
  92. package/src/commands/journal/search.ts +256 -0
  93. package/src/{lib → commands}/journal/templates.ts +1 -1
  94. package/src/config/settings.ts +35 -26
  95. package/plugins/tt-agentboard/bun.lock +0 -444
  96. package/plugins/tt-agentboard/packages/runtime/src/config.ts +0 -70
  97. package/plugins/tt-agentboard/packages/runtime/test/config.test.ts +0 -83
  98. package/plugins/tt-auto-claude/.claude-plugin/plugin.json +0 -8
  99. package/plugins/tt-auto-claude/commands/create-issue.md +0 -20
  100. package/plugins/tt-auto-claude/commands/list.md +0 -21
  101. package/plugins/tt-auto-claude/skills/auto-claude/SKILL.md +0 -71
  102. package/plugins/tt-core/promptfooconfig.interview-me.yaml +0 -155
  103. package/plugins/tt-core/promptfooconfig.refine-text.yaml +0 -242
  104. package/plugins/tt-core/promptfooconfig.tdd.yaml +0 -144
  105. package/plugins/tt-core/promptfooconfig.write-prd.yaml +0 -145
  106. package/src/commands/config.test.ts +0 -9
  107. package/src/lib/auto-claude/index.ts +0 -15
  108. package/src/lib/auto-claude/shell.ts +0 -6
  109. package/src/lib/graph/index.ts +0 -24
  110. package/src/lib/journal/index.ts +0 -11
  111. package/src/utils/git/exec.ts +0 -18
  112. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/build.ts +0 -0
  113. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/bunfig.toml +0 -0
  114. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/scripts/sessionizer.sh +0 -0
  115. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/DiffStats.tsx +0 -0
  116. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/SessionCard.tsx +0 -0
  117. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/detail-panel-height.ts +0 -0
  118. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/mux-context.ts +0 -0
  119. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/tsconfig.json +0 -0
  120. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/package.json +0 -0
  121. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/client.ts +0 -0
  122. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/index.ts +0 -0
  123. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/provider.ts +0 -0
  124. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/tsconfig.json +0 -0
  125. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/tracker.ts +0 -0
  126. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/amp.ts +0 -0
  127. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/codex.ts +0 -0
  128. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/opencode.ts +0 -0
  129. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/agent-watcher.ts +0 -0
  130. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/agent.ts +0 -0
  131. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/index.ts +0 -0
  132. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/mux.ts +0 -0
  133. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/debug.ts +0 -0
  134. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/mux/detect.ts +0 -0
  135. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/mux/registry.ts +0 -0
  136. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/context.ts +0 -0
  137. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/metadata-store.ts +0 -0
  138. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/port-scanner.ts +0 -0
  139. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/session-order.ts +0 -0
  140. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/sidebar-manager.ts +0 -0
  141. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/sidebar-width-sync.ts +0 -0
  142. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/themes.ts +0 -0
  143. /package/{plugins/tt-agentboard → packages/agentboard}/tsconfig.json +0 -0
  144. /package/{plugins/tt-core → packages/core}/.claude-plugin/plugin.json +0 -0
  145. /package/{plugins/tt-core → packages/core}/README.md +0 -0
  146. /package/{plugins/tt-core → packages/core}/commands/improve-architecture.md +0 -0
  147. /package/{plugins/tt-core → packages/core}/commands/interview-me.md +0 -0
  148. /package/{plugins/tt-core → packages/core}/commands/prd-to-issues.md +0 -0
  149. /package/{plugins/tt-core → packages/core}/commands/refine-text.md +0 -0
  150. /package/{plugins/tt-core → packages/core}/commands/task.md +0 -0
  151. /package/{plugins/tt-core → packages/core}/commands/tdd.md +0 -0
  152. /package/{plugins/tt-core → packages/core}/commands/write-prd.md +0 -0
  153. /package/{plugins/tt-core → packages/core}/skills/towles-tool/SKILL.md +0 -0
  154. /package/{src/utils → packages/shared/src}/date-utils.test.ts +0 -0
  155. /package/{src/utils → packages/shared/src}/date-utils.ts +0 -0
  156. /package/{src/utils → packages/shared/src}/fs.ts +0 -0
  157. /package/{src/utils → packages/shared/src}/git/branch-name.test.ts +0 -0
  158. /package/{src/utils → packages/shared/src}/git/branch-name.ts +0 -0
  159. /package/{src/utils → packages/shared/src}/git/gh-cli-wrapper.test.ts +0 -0
  160. /package/{src/utils → packages/shared/src}/render.test.ts +0 -0
  161. /package/{src/utils → packages/shared/src}/render.ts +0 -0
  162. /package/src/{lib → commands}/auto-claude/config.test.ts +0 -0
  163. /package/src/{lib → commands}/auto-claude/labels.test.ts +0 -0
  164. /package/src/{lib → commands}/auto-claude/pipeline.test.ts +0 -0
  165. /package/src/{lib → commands}/auto-claude/prompt-templates/01_plan.prompt.md +0 -0
  166. /package/src/{lib → commands}/auto-claude/prompt-templates/02_implement.prompt.md +0 -0
  167. /package/src/{lib → commands}/auto-claude/prompt-templates/03_simplify.prompt.md +0 -0
  168. /package/src/{lib → commands}/auto-claude/prompt-templates/04_review.prompt.md +0 -0
  169. /package/src/{lib → commands}/auto-claude/prompt-templates/CLAUDE.md +0 -0
  170. /package/src/{lib → commands}/auto-claude/prompt-templates/index.test.ts +0 -0
  171. /package/src/{lib → commands}/auto-claude/prompt-templates/index.ts +0 -0
  172. /package/src/{lib → commands}/auto-claude/run-claude.test.ts +0 -0
  173. /package/src/{lib → commands}/auto-claude/spawn-claude.ts +0 -0
  174. /package/src/{lib → commands}/auto-claude/steps/simple-steps.ts +0 -0
  175. /package/src/{lib → commands}/auto-claude/steps/steps.test.ts +0 -0
  176. /package/src/{lib → commands}/auto-claude/stream-parser.test.ts +0 -0
  177. /package/src/{lib → commands}/auto-claude/stream-parser.ts +0 -0
  178. /package/src/{lib → commands}/auto-claude/templates.test.ts +0 -0
  179. /package/src/{lib → commands}/auto-claude/templates.ts +0 -0
  180. /package/src/{lib → commands}/auto-claude/test-helpers.ts +0 -0
  181. /package/src/{lib → commands}/auto-claude/utils.test.ts +0 -0
  182. /package/src/{lib → commands}/graph/analyzer.ts +0 -0
  183. /package/src/{lib → commands}/graph/graph-template.html +0 -0
  184. /package/src/{lib → commands}/graph/parser.test.ts +0 -0
  185. /package/src/{lib → commands}/graph/parser.ts +0 -0
  186. /package/src/{lib → commands}/graph/render.ts +0 -0
  187. /package/src/{lib → commands}/graph/sessions.ts +0 -0
  188. /package/src/{lib → commands}/graph/tools.ts +0 -0
  189. /package/src/{lib → commands}/graph/treemap.ts +0 -0
  190. /package/src/{lib → commands}/journal/editor.ts +0 -0
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
  import type { AgentStatus } from "../contracts/agent";
5
+ import { TERMINAL_STATUSES } from "../contracts/agent";
5
6
  import type { SidebarPane } from "../contracts/mux";
6
7
  import type { ServerContext, PaneAgentPresence } from "./context";
7
8
  import { shell } from "./git-info";
@@ -98,6 +99,11 @@ function resolveClaudeCodePaneInfo(
98
99
  const threadId: string | undefined = data.sessionId;
99
100
  if (!threadId) return {};
100
101
  const journalInfo = resolveClaudeCodeJournalInfo(threadId);
102
+ // Process is alive (found via process tree), so terminal journal status
103
+ // is a between-turn artifact — override to running.
104
+ if (journalInfo.status && TERMINAL_STATUSES.has(journalInfo.status)) {
105
+ journalInfo.status = "running";
106
+ }
101
107
  return { threadId, ...journalInfo };
102
108
  } catch {
103
109
  return {};
@@ -1,10 +1,14 @@
1
1
  import type { AgentStatus, AgentEvent } from "./contracts/agent";
2
2
 
3
- export const SERVER_PORT = 4201;
4
- export const SERVER_HOST = "127.0.0.1";
3
+ export const DEFAULT_SERVER_PORT = 4201;
4
+ export const DEFAULT_SERVER_HOST = "127.0.0.1";
5
+
6
+ export const SERVER_PORT: number = Number(process.env.TT_AGENTBOARD_PORT) || DEFAULT_SERVER_PORT;
7
+ export const SERVER_HOST: string = process.env.TT_AGENTBOARD_HOST || DEFAULT_SERVER_HOST;
5
8
  export const PID_FILE = "/tmp/agentboard.pid";
6
9
  export const SERVER_IDLE_TIMEOUT_MS = 30_000;
7
10
  export const STUCK_RUNNING_TIMEOUT_MS = 3 * 60 * 1000;
11
+ export const JOURNAL_IDLE_TIMEOUT_MS = 120_000;
8
12
 
9
13
  export interface SessionData {
10
14
  name: string;
@@ -35,6 +39,7 @@ export interface ServerState {
35
39
  currentSession: string | null;
36
40
  theme: string | undefined;
37
41
  sidebarWidth: number;
42
+ preferredEditor: string;
38
43
  ts: number;
39
44
  }
40
45
 
@@ -9,5 +9,5 @@
9
9
  "noEmit": true,
10
10
  "types": ["bun-types"]
11
11
  },
12
- "include": ["src/**/*", "test/**/*"]
12
+ "include": ["src/**/*"]
13
13
  }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@towles/shared",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "types": "src/index.ts",
8
+ "dependencies": {
9
+ "consola": "^3.4.2"
10
+ },
11
+ "devDependencies": {
12
+ "@types/bun": "latest",
13
+ "typescript": "^5.8.3"
14
+ }
15
+ }
@@ -0,0 +1,41 @@
1
+ export interface XResult {
2
+ stdout: string;
3
+ stderr: string;
4
+ exitCode: number;
5
+ }
6
+
7
+ export interface XOptions {
8
+ throwOnError?: boolean;
9
+ cwd?: string;
10
+ }
11
+
12
+ export async function run(cmd: string, args: string[] = [], options?: XOptions): Promise<XResult> {
13
+ const cwd = options?.cwd ?? process.cwd();
14
+ const proc = Bun.spawn([cmd, ...args], { cwd, stdout: "pipe", stderr: "pipe" });
15
+ const exitCode = await proc.exited;
16
+ const [stdout, stderr] = await Promise.all([
17
+ new Response(proc.stdout).text(),
18
+ new Response(proc.stderr).text(),
19
+ ]);
20
+ if (options?.throwOnError && exitCode !== 0) {
21
+ throw new Error(`Command failed (exit ${exitCode}): ${cmd} ${args.join(" ")}\n${stderr}`);
22
+ }
23
+ return { stdout, stderr, exitCode };
24
+ }
25
+
26
+ export async function exec(cmd: string, args: string[]): Promise<string> {
27
+ const result = await run(cmd, args, { throwOnError: true });
28
+ return result.stdout.trim();
29
+ }
30
+
31
+ export async function execSafe(
32
+ cmd: string,
33
+ args: string[],
34
+ ): Promise<{ stdout: string; ok: boolean }> {
35
+ const result = await run(cmd, args);
36
+ return { stdout: result.stdout.trim(), ok: result.exitCode === 0 };
37
+ }
38
+
39
+ export async function git(args: string[]): Promise<string> {
40
+ return exec("git", args);
41
+ }
@@ -1,36 +1,32 @@
1
- import stripAnsi from "strip-ansi";
2
- import { x } from "tinyexec";
3
- import type { Output } from "tinyexec";
4
-
5
- import { execSafe } from "./exec.js";
1
+ import { exec, execSafe, run as defaultX } from "./exec.js";
2
+ import type { XResult } from "./exec.js";
6
3
 
7
4
  export type XFn = (
8
5
  cmd: string,
9
6
  args?: string[],
10
7
  opts?: Record<string, unknown>,
11
- ) => PromiseLike<Output>;
8
+ ) => PromiseLike<XResult>;
12
9
 
13
- export async function isGithubCliInstalled(exec: XFn = x as XFn): Promise<boolean> {
10
+ export async function isGithubCliInstalled(execFn: XFn = defaultX): Promise<boolean> {
14
11
  try {
15
- const proc = await exec("gh", ["--version"]);
12
+ const proc = await execFn("gh", ["--version"]);
16
13
  return proc.stdout.includes("https://github.com/cli/cli");
17
14
  } catch {
18
- // gh CLI not installed or not accessible
19
15
  return false;
20
16
  }
21
17
  }
22
18
 
23
19
  export async function gh<T = unknown>(args: string[]): Promise<T> {
24
- const result = await x("gh", args, { nodeOptions: { cwd: process.cwd() }, throwOnError: true });
25
- return JSON.parse(result.stdout.trim()) as T;
20
+ const stdout = await exec("gh", args);
21
+ return JSON.parse(stdout) as T;
26
22
  }
27
23
 
28
24
  export async function ghRaw(
29
25
  args: string[],
30
- exec?: (cmd: string, args: string[]) => Promise<{ stdout: string; ok: boolean }>,
26
+ execFn?: (cmd: string, args: string[]) => Promise<{ stdout: string; ok: boolean }>,
31
27
  ): Promise<string> {
32
- const execFn = exec ?? execSafe;
33
- const result = await execFn("gh", args);
28
+ const fn = execFn ?? execSafe;
29
+ const result = await fn("gh", args);
34
30
  return result.stdout;
35
31
  }
36
32
 
@@ -48,7 +44,7 @@ export async function getIssues({
48
44
  assignedToMe,
49
45
  cwd,
50
46
  label,
51
- exec = x as XFn,
47
+ exec: execFn = defaultX,
52
48
  }: {
53
49
  assignedToMe?: boolean;
54
50
  cwd: string;
@@ -65,9 +61,8 @@ export async function getIssues({
65
61
  args.push("--label", label);
66
62
  }
67
63
 
68
- const result = await exec("gh", args);
69
- // Setting NO_COLOR=1 didn't remove colors so had to use stripAnsi
70
- const stripped = stripAnsi(result.stdout);
64
+ const result = await execFn("gh", args);
65
+ const stripped = Bun.stripANSI(result.stdout);
71
66
 
72
67
  try {
73
68
  return JSON.parse(stripped) as Issue[];
@@ -0,0 +1,8 @@
1
+ export { ensureDir, fileExists, readFile, writeFile } from "./fs.js";
2
+ export { getTerminalColumns, limitText, printWithHexColor } from "./render.js";
3
+ export { formatDate, generateJournalFilename, getMondayOfWeek, getWeekInfo } from "./date-utils.js";
4
+ export { exec, execSafe, git, run } from "./git/exec.js";
5
+ export type { XResult, XOptions } from "./git/exec.js";
6
+ export { gh, ghRaw, getIssues, isGithubCliInstalled } from "./git/gh-cli-wrapper.js";
7
+ export type { Issue, XFn } from "./git/gh-cli-wrapper.js";
8
+ export { createBranchNameFromIssue } from "./git/branch-name.js";
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "resolveJsonModule": true,
8
+ "strict": true,
9
+ "strictNullChecks": true,
10
+ "noEmit": true,
11
+ "esModuleInterop": true,
12
+ "skipLibCheck": true,
13
+ "types": ["bun-types"]
14
+ },
15
+ "include": ["src/**/*.ts"]
16
+ }
package/src/cli.ts CHANGED
@@ -4,7 +4,7 @@ import { version } from "../package.json";
4
4
  export const main = defineCommand({
5
5
  meta: { name: "tt", version, description: "towles-tool — personal CLI utilities" },
6
6
  subCommands: {
7
- config: () => import("./commands/config.js").then((m) => m.default),
7
+ config: () => import("./commands/config/index.js").then((m) => m.default),
8
8
  doctor: () => import("./commands/doctor.js").then((m) => m.default),
9
9
  install: () => import("./commands/install.js").then((m) => m.default),
10
10
  agentboard: () => import("./commands/agentboard.js").then((m) => m.default),
@@ -6,10 +6,8 @@ import consola from "consola";
6
6
  import { colors } from "consola/utils";
7
7
  import { debugArg } from "./shared.js";
8
8
 
9
- const SERVER_HOST = "127.0.0.1";
10
- const SERVER_PORT = 4201;
11
-
12
- const PLUGIN_DIR = resolve(import.meta.dirname, "../../plugins/tt-agentboard");
9
+ const SERVER_HOST = process.env.TT_AGENTBOARD_HOST || "127.0.0.1";
10
+ const SERVER_PORT = Number(process.env.TT_AGENTBOARD_PORT) || 4201;
13
11
 
14
12
  // Keybinding defaults
15
13
  const DEFAULT_KEY = "a";
@@ -20,37 +18,18 @@ const MARKER = "# agentboard";
20
18
  function findTmuxConf(): string | null {
21
19
  const candidates = [resolve(process.env.HOME ?? "~", ".config/tmux/tmux.conf")];
22
20
  for (const path of candidates) {
23
- try {
24
- const real = existsSync(path) ? path : null;
25
- if (real) return real;
26
- } catch {
27
- continue;
28
- }
21
+ if (existsSync(path)) return path;
29
22
  }
30
23
  return null;
31
24
  }
32
25
 
33
- function ensureDeps(): void {
26
+ function ensureBun(): void {
34
27
  try {
35
28
  execSync("bun --version", { stdio: "pipe" });
36
29
  } catch {
37
30
  consola.error("bun is required but not found. Install: https://bun.sh");
38
31
  process.exit(1);
39
32
  }
40
-
41
- if (!existsSync(PLUGIN_DIR)) {
42
- consola.error(`Agentboard plugin directory not found: ${PLUGIN_DIR}`);
43
- consola.error(
44
- "If installed globally, ensure the 'plugins' directory is included in the package.",
45
- );
46
- process.exit(1);
47
- }
48
-
49
- const runtimeNodeModules = resolve(PLUGIN_DIR, "packages/runtime/node_modules");
50
- if (!existsSync(runtimeNodeModules)) {
51
- consola.info("Installing agentboard dependencies...");
52
- execSync("bun install", { cwd: PLUGIN_DIR, stdio: "inherit" });
53
- }
54
33
  }
55
34
 
56
35
  function reloadTmux(): void {
@@ -111,8 +90,6 @@ function showKeys(): void {
111
90
  }
112
91
 
113
92
  function setup(): void {
114
- ensureDeps();
115
-
116
93
  const confPath = findTmuxConf();
117
94
  if (!confPath) {
118
95
  consola.warn("No tmux.conf found. Add this line manually:");
@@ -169,14 +146,14 @@ function uninstall(): void {
169
146
  }
170
147
 
171
148
  const content = readFileSync(editPath, "utf8");
172
- if (!content.includes("agentboard")) {
149
+ if (!content.includes(MARKER) && !content.includes(RUN_SHELL_LINE)) {
173
150
  consola.info("agentboard not found in tmux.conf");
174
151
  return;
175
152
  }
176
153
 
177
154
  const newContent = content
178
155
  .split("\n")
179
- .filter((line) => !line.includes("agentboard"))
156
+ .filter((line) => !line.includes(MARKER) && !line.includes(RUN_SHELL_LINE))
180
157
  .join("\n")
181
158
  .replace(/\n{3,}/g, "\n\n");
182
159
 
@@ -186,18 +163,15 @@ function uninstall(): void {
186
163
  }
187
164
 
188
165
  function startServer(): void {
189
- ensureDeps();
166
+ ensureBun();
190
167
 
191
- const serverEntry = resolve(PLUGIN_DIR, "apps/server/src/main.ts");
168
+ const agentboardDir = resolve(import.meta.dirname, "../../packages/agentboard");
169
+ const serverEntry = resolve(agentboardDir, "apps/server/src/main.ts");
192
170
  consola.info("Starting agentboard server (foreground, Ctrl+C to stop)...");
193
171
 
194
172
  execSync(`bun run ${serverEntry}`, {
195
173
  stdio: "inherit",
196
- cwd: PLUGIN_DIR,
197
- env: {
198
- ...process.env,
199
- AGENTBOARD_DIR: PLUGIN_DIR,
200
- },
174
+ cwd: agentboardDir,
201
175
  });
202
176
  }
203
177
 
@@ -215,13 +189,10 @@ async function serverAlive(): Promise<boolean> {
215
189
  async function ensureServerUp(): Promise<boolean> {
216
190
  if (await serverAlive()) return true;
217
191
 
218
- const serverEntry = resolve(PLUGIN_DIR, "apps/server/src/main.ts");
219
192
  consola.info("Starting agentboard server...");
220
- const child = spawn("bun", ["run", serverEntry], {
193
+ const child = spawn("tt", ["agentboard", "server"], {
221
194
  stdio: "ignore",
222
- cwd: PLUGIN_DIR,
223
195
  detached: true,
224
- env: { ...process.env, AGENTBOARD_DIR: PLUGIN_DIR },
225
196
  });
226
197
  child.unref();
227
198
 
@@ -262,7 +233,9 @@ function findSidebarPane(windowId: string): string | null {
262
233
  const [paneId, title] = line.split(" ", 2);
263
234
  if (title === "agentboard-sidebar" && paneId) return paneId;
264
235
  }
265
- } catch {}
236
+ } catch (err) {
237
+ consola.debug(`findSidebarPane failed for window ${windowId}:`, err);
238
+ }
266
239
  return null;
267
240
  }
268
241
 
@@ -360,12 +333,13 @@ async function runFocus(): Promise<void> {
360
333
  return;
361
334
  }
362
335
 
363
- // Otherwise, ensure server + toggle sidebar on
336
+ // Otherwise, ensure server + open sidebar
364
337
  if (!(await ensureServerUp())) process.exit(0);
365
338
  const ctx = tmuxContext();
366
- await fetch(`http://${SERVER_HOST}:${SERVER_PORT}/toggle`, { method: "POST", body: ctx }).catch(
367
- () => {},
368
- );
339
+ await fetch(`http://${SERVER_HOST}:${SERVER_PORT}/ensure-sidebar`, {
340
+ method: "POST",
341
+ body: ctx,
342
+ }).catch(() => {});
369
343
 
370
344
  // Wait for sidebar pane to appear
371
345
  for (let i = 0; i < 20; i++) {
@@ -381,7 +355,7 @@ async function runFocus(): Promise<void> {
381
355
  }
382
356
 
383
357
  async function restart(): Promise<void> {
384
- ensureDeps();
358
+ ensureBun();
385
359
 
386
360
  // 1. Kill stash sessions left over from hidden sidebars
387
361
  try {
@@ -407,35 +381,47 @@ async function restart(): Promise<void> {
407
381
  }
408
382
  consola.success("Server is running");
409
383
 
410
- // 3. Toggle sidebar on (the server spawns sidebars in all active windows)
384
+ // 3. Bootstrap sidebars refresh session list, then ensure-sidebar for
385
+ // each active client's window so sidebars appear without interaction.
386
+ const base = `http://${SERVER_HOST}:${SERVER_PORT}`;
387
+ await fetch(`${base}/refresh`, { method: "POST", signal: AbortSignal.timeout(2000) }).catch(
388
+ () => {},
389
+ );
390
+
411
391
  try {
412
- const res = await fetch(`http://${SERVER_HOST}:${SERVER_PORT}/toggle`, {
413
- method: "POST",
414
- body: "",
415
- signal: AbortSignal.timeout(2000),
416
- });
417
- if (res.ok) {
418
- consola.success("Sidebar toggled on for all sessions");
419
- } else {
420
- consola.warn(`Toggle returned: ${res.status}`);
421
- }
392
+ const clients = spawnSync(
393
+ "tmux",
394
+ ["list-clients", "-F", "#{client_tty}|#{session_name}|#{window_id}"],
395
+ {
396
+ encoding: "utf8",
397
+ stdio: ["pipe", "pipe", "pipe"],
398
+ },
399
+ );
400
+ const lines = (clients.stdout ?? "").trim().split("\n").filter(Boolean);
401
+ await Promise.allSettled(
402
+ lines.map((ctx) =>
403
+ fetch(`${base}/ensure-sidebar`, {
404
+ method: "POST",
405
+ body: ctx,
406
+ signal: AbortSignal.timeout(2000),
407
+ }),
408
+ ),
409
+ );
410
+ consola.success(`Sidebars ensured for ${lines.length} client(s)`);
422
411
  } catch (err) {
423
- consola.error("Failed to toggle sidebar:", err);
412
+ consola.error("Failed to bootstrap sidebars:", err);
424
413
  }
425
414
  }
426
415
 
427
416
  function startTui(): void {
428
- ensureDeps();
417
+ ensureBun();
429
418
 
430
- const tuiEntry = resolve(PLUGIN_DIR, "apps/tui/src/index.tsx");
419
+ const agentboardDir = resolve(import.meta.dirname, "../../packages/agentboard");
420
+ const tuiEntry = resolve(agentboardDir, "apps/tui/src/index.tsx");
431
421
 
432
422
  execSync(`bun run ${tuiEntry}`, {
433
423
  stdio: "inherit",
434
- cwd: resolve(PLUGIN_DIR, "apps/tui"),
435
- env: {
436
- ...process.env,
437
- AGENTBOARD_DIR: PLUGIN_DIR,
438
- },
424
+ cwd: resolve(agentboardDir, "apps/tui"),
439
425
  });
440
426
  }
441
427
 
@@ -463,8 +449,6 @@ export default defineCommand({
463
449
  startServer();
464
450
  break;
465
451
  case "tui":
466
- startTui();
467
- break;
468
452
  case "start":
469
453
  startTui();
470
454
  break;
@@ -4,7 +4,7 @@ import { join } from "node:path";
4
4
  import consola from "consola";
5
5
  import pc from "picocolors";
6
6
 
7
- import { readFile } from "../../utils/fs.js";
7
+ import { readFile } from "@towles/shared";
8
8
  import { getConfig } from "./config.js";
9
9
  import { sleep } from "./shell.js";
10
10
  import { spawnClaude as defaultSpawnClaude } from "./spawn-claude.js";
@@ -0,0 +1,79 @@
1
+ import { AutoClaudeConfigSchema } from "./config.js";
2
+ import type { AutoClaudeConfig } from "./config.js";
3
+
4
+ export const CONFIG_DIR = ".auto-claude";
5
+ export const CONFIG_FILENAME = "config.json";
6
+ export const CONFIG_PATH = `${CONFIG_DIR}/${CONFIG_FILENAME}`;
7
+
8
+ export interface ConfigInitInput {
9
+ triggerLabel: string;
10
+ mainBranch: string;
11
+ scopePath: string;
12
+ model: string;
13
+ repo: string;
14
+ }
15
+
16
+ /**
17
+ * Build a full AutoClaudeConfig from user inputs, applying schema defaults.
18
+ */
19
+ export function buildConfig(input: ConfigInitInput): AutoClaudeConfig {
20
+ return AutoClaudeConfigSchema.parse({
21
+ triggerLabel: input.triggerLabel,
22
+ mainBranch: input.mainBranch,
23
+ scopePath: input.scopePath,
24
+ model: input.model,
25
+ repo: input.repo,
26
+ });
27
+ }
28
+
29
+ /**
30
+ * Validate that a trigger label is non-empty and contains no spaces.
31
+ */
32
+ export function validateTriggerLabel(label: string): string | true {
33
+ const trimmed = label.trim();
34
+ if (trimmed.length === 0) return "Trigger label cannot be empty";
35
+ if (/\s/.test(trimmed)) return "Trigger label cannot contain spaces";
36
+ return true;
37
+ }
38
+
39
+ /**
40
+ * Validate that a branch name is non-empty.
41
+ */
42
+ export function validateBranchName(name: string): string | true {
43
+ const trimmed = name.trim();
44
+ if (trimmed.length === 0) return "Branch name cannot be empty";
45
+ return true;
46
+ }
47
+
48
+ /**
49
+ * Validate that a scope path is non-empty.
50
+ */
51
+ export function validateScopePath(path: string): string | true {
52
+ const trimmed = path.trim();
53
+ if (trimmed.length === 0) return "Scope path cannot be empty";
54
+ return true;
55
+ }
56
+
57
+ /**
58
+ * Format the config as a human-readable summary for confirmation.
59
+ */
60
+ export function formatConfigSummary(config: AutoClaudeConfig): string {
61
+ return [
62
+ ` repo: ${config.repo}`,
63
+ ` triggerLabel: ${config.triggerLabel}`,
64
+ ` mainBranch: ${config.mainBranch}`,
65
+ ` scopePath: ${config.scopePath}`,
66
+ ` model: ${config.model}`,
67
+ ` remote: ${config.remote}`,
68
+ ` maxIterations: ${config.maxImplementIterations}`,
69
+ ` maxReviewRetries: ${config.maxReviewRetries}`,
70
+ ` loopInterval: ${config.loopIntervalMinutes}min`,
71
+ ].join("\n");
72
+ }
73
+
74
+ /**
75
+ * Serialize config to JSON for writing to disk.
76
+ */
77
+ export function serializeConfig(config: AutoClaudeConfig): string {
78
+ return JSON.stringify(config, null, 2) + "\n";
79
+ }
@@ -0,0 +1,137 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ buildConfig,
5
+ formatConfigSummary,
6
+ serializeConfig,
7
+ validateBranchName,
8
+ validateScopePath,
9
+ validateTriggerLabel,
10
+ } from "./config-init-helpers";
11
+
12
+ describe("validateTriggerLabel", () => {
13
+ it("accepts a valid label", () => {
14
+ expect(validateTriggerLabel("auto-claude")).toBe(true);
15
+ });
16
+
17
+ it("rejects empty string", () => {
18
+ expect(validateTriggerLabel("")).toBe("Trigger label cannot be empty");
19
+ });
20
+
21
+ it("rejects whitespace-only", () => {
22
+ expect(validateTriggerLabel(" ")).toBe("Trigger label cannot be empty");
23
+ });
24
+
25
+ it("rejects label with spaces", () => {
26
+ expect(validateTriggerLabel("auto claude")).toBe("Trigger label cannot contain spaces");
27
+ });
28
+ });
29
+
30
+ describe("validateBranchName", () => {
31
+ it("accepts a valid branch name", () => {
32
+ expect(validateBranchName("main")).toBe(true);
33
+ });
34
+
35
+ it("rejects empty string", () => {
36
+ expect(validateBranchName("")).toBe("Branch name cannot be empty");
37
+ });
38
+ });
39
+
40
+ describe("validateScopePath", () => {
41
+ it("accepts a valid path", () => {
42
+ expect(validateScopePath(".")).toBe(true);
43
+ });
44
+
45
+ it("accepts a subdirectory", () => {
46
+ expect(validateScopePath("src/lib")).toBe(true);
47
+ });
48
+
49
+ it("rejects empty string", () => {
50
+ expect(validateScopePath("")).toBe("Scope path cannot be empty");
51
+ });
52
+ });
53
+
54
+ describe("buildConfig", () => {
55
+ it("builds config with provided values", () => {
56
+ const config = buildConfig({
57
+ triggerLabel: "my-label",
58
+ mainBranch: "develop",
59
+ scopePath: "src/",
60
+ model: "sonnet",
61
+ repo: "owner/repo",
62
+ });
63
+
64
+ expect(config.triggerLabel).toBe("my-label");
65
+ expect(config.mainBranch).toBe("develop");
66
+ expect(config.scopePath).toBe("src/");
67
+ expect(config.model).toBe("sonnet");
68
+ expect(config.repo).toBe("owner/repo");
69
+ });
70
+
71
+ it("applies schema defaults for fields not in input", () => {
72
+ const config = buildConfig({
73
+ triggerLabel: "auto-claude",
74
+ mainBranch: "main",
75
+ scopePath: ".",
76
+ model: "opus",
77
+ repo: "owner/repo",
78
+ });
79
+
80
+ expect(config.remote).toBe("origin");
81
+ expect(config.maxImplementIterations).toBe(5);
82
+ expect(config.maxReviewRetries).toBe(2);
83
+ expect(config.loopIntervalMinutes).toBe(30);
84
+ });
85
+ });
86
+
87
+ describe("formatConfigSummary", () => {
88
+ it("includes all key fields", () => {
89
+ const config = buildConfig({
90
+ triggerLabel: "auto-claude",
91
+ mainBranch: "main",
92
+ scopePath: ".",
93
+ model: "opus",
94
+ repo: "owner/repo",
95
+ });
96
+
97
+ const summary = formatConfigSummary(config);
98
+ expect(summary).toContain("owner/repo");
99
+ expect(summary).toContain("auto-claude");
100
+ expect(summary).toContain("main");
101
+ expect(summary).toContain("opus");
102
+ expect(summary).toContain("origin");
103
+ expect(summary).toContain("30min");
104
+ });
105
+ });
106
+
107
+ describe("serializeConfig", () => {
108
+ it("returns valid JSON with trailing newline", () => {
109
+ const config = buildConfig({
110
+ triggerLabel: "auto-claude",
111
+ mainBranch: "main",
112
+ scopePath: ".",
113
+ model: "opus",
114
+ repo: "owner/repo",
115
+ });
116
+
117
+ const json = serializeConfig(config);
118
+ expect(json.endsWith("\n")).toBe(true);
119
+
120
+ const parsed = JSON.parse(json);
121
+ expect(parsed.repo).toBe("owner/repo");
122
+ expect(parsed.triggerLabel).toBe("auto-claude");
123
+ });
124
+
125
+ it("is pretty-printed with 2-space indent", () => {
126
+ const config = buildConfig({
127
+ triggerLabel: "test",
128
+ mainBranch: "main",
129
+ scopePath: ".",
130
+ model: "opus",
131
+ repo: "o/r",
132
+ });
133
+
134
+ const json = serializeConfig(config);
135
+ expect(json).toContain(' "repo"');
136
+ });
137
+ });