@towles/tool 0.0.106 → 0.0.108

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 (76) hide show
  1. package/README.md +7 -1
  2. package/package.json +2 -1
  3. package/plugins/tt-agentboard/README.md +160 -0
  4. package/plugins/tt-agentboard/apps/server/package.json +20 -0
  5. package/plugins/tt-agentboard/apps/server/src/main.ts +60 -0
  6. package/plugins/tt-agentboard/apps/tui/build.ts +11 -0
  7. package/plugins/tt-agentboard/apps/tui/bunfig.toml +1 -0
  8. package/plugins/tt-agentboard/apps/tui/package.json +23 -0
  9. package/plugins/tt-agentboard/apps/tui/scripts/sessionizer.sh +36 -0
  10. package/plugins/tt-agentboard/apps/tui/src/components/DetailPanel.tsx +350 -0
  11. package/plugins/tt-agentboard/apps/tui/src/components/DiffStats.tsx +33 -0
  12. package/plugins/tt-agentboard/apps/tui/src/components/SessionCard.tsx +177 -0
  13. package/plugins/tt-agentboard/apps/tui/src/components/StatusBar.tsx +49 -0
  14. package/plugins/tt-agentboard/apps/tui/src/constants.ts +46 -0
  15. package/plugins/tt-agentboard/apps/tui/src/detail-panel-height.ts +21 -0
  16. package/plugins/tt-agentboard/apps/tui/src/index.tsx +880 -0
  17. package/plugins/tt-agentboard/apps/tui/src/mux-context.ts +61 -0
  18. package/plugins/tt-agentboard/apps/tui/tsconfig.json +15 -0
  19. package/plugins/tt-agentboard/bun.lock +444 -0
  20. package/plugins/tt-agentboard/package.json +26 -0
  21. package/plugins/tt-agentboard/packages/mux-tmux/package.json +14 -0
  22. package/plugins/tt-agentboard/packages/mux-tmux/src/client.ts +550 -0
  23. package/plugins/tt-agentboard/packages/mux-tmux/src/index.ts +18 -0
  24. package/plugins/tt-agentboard/packages/mux-tmux/src/provider.ts +259 -0
  25. package/plugins/tt-agentboard/packages/mux-tmux/tsconfig.json +13 -0
  26. package/plugins/tt-agentboard/packages/runtime/package.json +14 -0
  27. package/plugins/tt-agentboard/packages/runtime/src/agents/tracker.ts +233 -0
  28. package/plugins/tt-agentboard/packages/runtime/src/agents/watchers/amp.ts +316 -0
  29. package/plugins/tt-agentboard/packages/runtime/src/agents/watchers/claude-code.ts +374 -0
  30. package/plugins/tt-agentboard/packages/runtime/src/agents/watchers/codex.ts +364 -0
  31. package/plugins/tt-agentboard/packages/runtime/src/agents/watchers/opencode.ts +249 -0
  32. package/plugins/tt-agentboard/packages/runtime/src/config.ts +70 -0
  33. package/plugins/tt-agentboard/packages/runtime/src/contracts/agent-watcher.ts +38 -0
  34. package/plugins/tt-agentboard/packages/runtime/src/contracts/agent.ts +16 -0
  35. package/plugins/tt-agentboard/packages/runtime/src/contracts/index.ts +3 -0
  36. package/plugins/tt-agentboard/packages/runtime/src/contracts/mux.ts +148 -0
  37. package/plugins/tt-agentboard/packages/runtime/src/debug.ts +19 -0
  38. package/plugins/tt-agentboard/packages/runtime/src/index.ts +69 -0
  39. package/plugins/tt-agentboard/packages/runtime/src/mux/detect.ts +20 -0
  40. package/plugins/tt-agentboard/packages/runtime/src/mux/registry.ts +45 -0
  41. package/plugins/tt-agentboard/packages/runtime/src/plugins/loader.ts +152 -0
  42. package/plugins/tt-agentboard/packages/runtime/src/server/context.ts +112 -0
  43. package/plugins/tt-agentboard/packages/runtime/src/server/git-info.ts +164 -0
  44. package/plugins/tt-agentboard/packages/runtime/src/server/index.ts +1753 -0
  45. package/plugins/tt-agentboard/packages/runtime/src/server/launcher.ts +71 -0
  46. package/plugins/tt-agentboard/packages/runtime/src/server/metadata-store.ts +86 -0
  47. package/plugins/tt-agentboard/packages/runtime/src/server/pane-scanner.ts +327 -0
  48. package/plugins/tt-agentboard/packages/runtime/src/server/port-scanner.ts +155 -0
  49. package/plugins/tt-agentboard/packages/runtime/src/server/session-order.ts +127 -0
  50. package/plugins/tt-agentboard/packages/runtime/src/server/sidebar-manager.ts +232 -0
  51. package/plugins/tt-agentboard/packages/runtime/src/server/sidebar-width-sync.ts +66 -0
  52. package/plugins/tt-agentboard/packages/runtime/src/shared.ts +179 -0
  53. package/plugins/tt-agentboard/packages/runtime/src/themes.ts +750 -0
  54. package/plugins/tt-agentboard/packages/runtime/test/config.test.ts +83 -0
  55. package/plugins/tt-agentboard/packages/runtime/test/tracker.test.ts +172 -0
  56. package/plugins/tt-agentboard/packages/runtime/tsconfig.json +13 -0
  57. package/plugins/tt-agentboard/tsconfig.json +19 -0
  58. package/plugins/tt-auto-claude/.claude-plugin/plugin.json +8 -0
  59. package/plugins/tt-auto-claude/commands/create-issue.md +20 -0
  60. package/plugins/tt-auto-claude/commands/list.md +21 -0
  61. package/plugins/tt-auto-claude/skills/auto-claude/SKILL.md +71 -0
  62. package/plugins/tt-core/.claude-plugin/plugin.json +8 -0
  63. package/plugins/tt-core/README.md +18 -0
  64. package/plugins/tt-core/commands/improve-architecture.md +66 -0
  65. package/plugins/tt-core/commands/interview-me.md +38 -0
  66. package/plugins/tt-core/commands/prd-to-issues.md +49 -0
  67. package/plugins/tt-core/commands/refine-text.md +30 -0
  68. package/plugins/tt-core/commands/task.md +37 -0
  69. package/plugins/tt-core/commands/tdd.md +69 -0
  70. package/plugins/tt-core/commands/write-prd.md +69 -0
  71. package/plugins/tt-core/promptfooconfig.interview-me.yaml +155 -0
  72. package/plugins/tt-core/promptfooconfig.refine-text.yaml +242 -0
  73. package/plugins/tt-core/promptfooconfig.tdd.yaml +144 -0
  74. package/plugins/tt-core/promptfooconfig.write-prd.yaml +145 -0
  75. package/plugins/tt-core/skills/towles-tool/SKILL.md +35 -0
  76. package/src/commands/agentboard.ts +19 -2
@@ -0,0 +1,38 @@
1
+ import type { AgentEvent } from "./agent";
2
+
3
+ /**
4
+ * Callback context provided by the server to each watcher.
5
+ * Lets watchers resolve project directories to mux session names
6
+ * and emit events without knowing about server internals.
7
+ */
8
+ export interface AgentWatcherContext {
9
+ /** Resolve a project directory path to a mux session name, or null if unmatched */
10
+ resolveSession(projectDir: string): string | null;
11
+ /** Emit an agent event (applied to tracker + broadcast automatically) */
12
+ emit(event: AgentEvent): void;
13
+ }
14
+
15
+ /**
16
+ * Interface for agent watchers that detect agent status by watching
17
+ * external data sources (thread files, databases, etc).
18
+ *
19
+ * Implementations:
20
+ * - amp: watches ~/.local/share/amp/threads/*.json
21
+ * - claude-code: watches ~/.claude/projects/ JSONL files
22
+ * - codex: watches ~/.codex/sessions/ JSONL transcripts
23
+ * - opencode: polls OpenCode SQLite database
24
+ *
25
+ * To add a new watcher:
26
+ * 1. Create a file implementing AgentWatcher
27
+ * 2. Register it via PluginAPI.registerWatcher() or in the server entry point
28
+ */
29
+ export interface AgentWatcher {
30
+ /** Unique name for this watcher (e.g. "amp", "claude-code") */
31
+ readonly name: string;
32
+
33
+ /** Start watching. Called once by the server with the watcher context. */
34
+ start(ctx: AgentWatcherContext): void;
35
+
36
+ /** Stop watching and clean up resources. */
37
+ stop(): void;
38
+ }
@@ -0,0 +1,16 @@
1
+ export type AgentStatus = "idle" | "running" | "done" | "error" | "waiting" | "interrupted";
2
+
3
+ export interface AgentEvent {
4
+ agent: string;
5
+ session: string;
6
+ status: AgentStatus;
7
+ ts: number;
8
+ threadId?: string;
9
+ threadName?: string;
10
+ /** Set by tracker when serializing for the TUI — true if user hasn't seen this terminal state */
11
+ unseen?: boolean;
12
+ /** Set by pane scanner — the tmux pane ID where this agent was detected */
13
+ paneId?: string;
14
+ }
15
+
16
+ export const TERMINAL_STATUSES = new Set<AgentStatus>(["done", "error", "interrupted"]);
@@ -0,0 +1,3 @@
1
+ export type { AgentStatus, AgentEvent } from "./agent";
2
+ export { TERMINAL_STATUSES } from "./agent";
3
+ export type { MuxProvider, MuxSessionInfo } from "./mux";
@@ -0,0 +1,148 @@
1
+ // ─── Specification version ───────────────────────────────────────────────────
2
+ // Like ai-sdk's specificationVersion — a literal discriminant for version compat.
3
+
4
+ export type MuxSpecificationVersion = "v1";
5
+
6
+ // ─── Core data types ─────────────────────────────────────────────────────────
7
+
8
+ export interface MuxSessionInfo {
9
+ readonly name: string;
10
+ readonly createdAt: number;
11
+ readonly dir: string;
12
+ readonly windows: number;
13
+ }
14
+
15
+ export interface ActiveWindow {
16
+ readonly id: string;
17
+ readonly sessionName: string;
18
+ readonly active: boolean;
19
+ }
20
+
21
+ export interface SidebarPane {
22
+ readonly paneId: string;
23
+ readonly sessionName: string;
24
+ readonly windowId: string;
25
+ readonly width?: number;
26
+ readonly windowWidth?: number;
27
+ }
28
+
29
+ /** Position for sidebar placement */
30
+ export type SidebarPosition = "left" | "right";
31
+
32
+ /** Provider-specific metadata (escape hatch — like ai-sdk's providerMetadata) */
33
+ export type MuxProviderMetadata = Record<string, Record<string, unknown>>;
34
+
35
+ // ─── Capability interfaces ───────────────────────────────────────────────────
36
+ // Split from one monolith into composable traits. Providers implement what they
37
+ // support. The server narrows with type guards, not NonNullable hacks.
38
+
39
+ /**
40
+ * Core mux operations — every provider MUST implement this.
41
+ *
42
+ * Like ai-sdk's ProviderV4 required methods (languageModel, embeddingModel).
43
+ */
44
+ export interface MuxProviderV1 {
45
+ readonly specificationVersion: "v1";
46
+ readonly name: string;
47
+
48
+ // Session CRUD
49
+ listSessions(): MuxSessionInfo[];
50
+ switchSession(name: string, clientTty?: string): void;
51
+ getCurrentSession(): string | null;
52
+ getSessionDir(name: string): string;
53
+ getPaneCount(name: string): number;
54
+ getClientTty(): string;
55
+ createSession(name?: string, dir?: string): void;
56
+ killSession(name: string): void;
57
+
58
+ // Hooks
59
+ setupHooks(serverHost: string, serverPort: number): void;
60
+ cleanupHooks(): void;
61
+ }
62
+
63
+ /**
64
+ * Window/tab awareness — providers that can enumerate their windows/tabs.
65
+ */
66
+ export interface WindowCapable {
67
+ listActiveWindows(): ActiveWindow[];
68
+ getCurrentWindowId(): string | null;
69
+ }
70
+
71
+ /**
72
+ * Sidebar management — providers that can spawn/manage sidebar panes.
73
+ */
74
+ export interface SidebarCapable {
75
+ listSidebarPanes(sessionName?: string): SidebarPane[];
76
+ spawnSidebar(
77
+ sessionName: string,
78
+ windowId: string,
79
+ width: number,
80
+ position: SidebarPosition,
81
+ ): string | null;
82
+ hideSidebar(paneId: string): void;
83
+ killSidebarPane(paneId: string): void;
84
+ resizeSidebarPane(paneId: string, width: number): void;
85
+ cleanupSidebar(): void;
86
+ }
87
+
88
+ /**
89
+ * Batch operations — providers that can fetch data in bulk for performance.
90
+ */
91
+ export interface BatchCapable {
92
+ getAllPaneCounts(): Map<string, number>;
93
+ }
94
+
95
+ // ─── Composite types ─────────────────────────────────────────────────────────
96
+
97
+ /**
98
+ * A fully-featured provider with all capabilities.
99
+ * Most providers won't implement everything — use the type guards below to narrow.
100
+ */
101
+ export type FullMuxProvider = MuxProviderV1 & WindowCapable & SidebarCapable & BatchCapable;
102
+
103
+ /**
104
+ * The union type the server accepts — core is required, capabilities are optional.
105
+ *
106
+ * Like ai-sdk's LanguageModel = V2 | V3 | V4 — accepts any level of capability.
107
+ */
108
+ export type MuxProvider = MuxProviderV1 & Partial<WindowCapable & SidebarCapable & BatchCapable>;
109
+
110
+ // ─── Type guards ─────────────────────────────────────────────────────────────
111
+ // Runtime narrowing — like ai-sdk's isInstance() pattern, but for capabilities.
112
+
113
+ /** Check if a provider supports window operations */
114
+ export function isWindowCapable(p: MuxProvider): p is MuxProviderV1 & WindowCapable {
115
+ return typeof p.listActiveWindows === "function" && typeof p.getCurrentWindowId === "function";
116
+ }
117
+
118
+ /** Check if a provider supports sidebar operations */
119
+ export function isSidebarCapable(p: MuxProvider): p is MuxProviderV1 & SidebarCapable {
120
+ return (
121
+ typeof p.listSidebarPanes === "function" &&
122
+ typeof p.spawnSidebar === "function" &&
123
+ typeof p.hideSidebar === "function" &&
124
+ typeof p.killSidebarPane === "function" &&
125
+ typeof p.resizeSidebarPane === "function" &&
126
+ typeof p.cleanupSidebar === "function"
127
+ );
128
+ }
129
+
130
+ /** Check if a provider supports batch operations */
131
+ export function isBatchCapable(p: MuxProvider): p is MuxProviderV1 & BatchCapable {
132
+ return typeof p.getAllPaneCounts === "function";
133
+ }
134
+
135
+ /** Check if a provider supports full sidebar management (window + sidebar) */
136
+ export function isFullSidebarCapable(
137
+ p: MuxProvider,
138
+ ): p is MuxProviderV1 & WindowCapable & SidebarCapable {
139
+ return isWindowCapable(p) && isSidebarCapable(p);
140
+ }
141
+
142
+ // ─── Provider settings ───────────────────────────────────────────────────────
143
+ // Like ai-sdk's OpenAIProviderSettings — each provider can extend this.
144
+
145
+ export interface MuxProviderSettings {
146
+ /** Override the provider name (for custom/wrapped providers) */
147
+ name?: string;
148
+ }
@@ -0,0 +1,19 @@
1
+ import { appendFileSync } from "node:fs";
2
+
3
+ export const DEBUG_LOG = "/tmp/agentboard-debug.log";
4
+ export const TUI_RESIZE_LOG = "/tmp/agentboard-tui-resize.log";
5
+ export const TUI_AGENT_CLICK_LOG = "/tmp/agentboard-tui-agent-click.log";
6
+ export const SERVER_ERR_LOG = "/tmp/agentboard-server-err.log";
7
+ export const INSTALL_LOG = "/tmp/agentboard-install.log";
8
+
9
+ const DEBUG_ENABLED = !!process.env.TT_AGENTBOARD_DEBUG;
10
+
11
+ export function debugLog(category: string, msg: string, data?: Record<string, unknown>): void {
12
+ if (!DEBUG_ENABLED) return;
13
+ const ts = new Date().toISOString().slice(11, 23);
14
+ const extra = data ? " " + JSON.stringify(data) : "";
15
+ const line = `[${ts}] [${category}] ${msg}${extra}\n`;
16
+ try {
17
+ appendFileSync(DEBUG_LOG, line);
18
+ } catch {}
19
+ }
@@ -0,0 +1,69 @@
1
+ export type {
2
+ MuxProvider,
3
+ MuxProviderV1,
4
+ MuxSessionInfo,
5
+ ActiveWindow,
6
+ SidebarPane,
7
+ SidebarPosition,
8
+ WindowCapable,
9
+ SidebarCapable,
10
+ BatchCapable,
11
+ FullMuxProvider,
12
+ MuxProviderSettings,
13
+ } from "./contracts/mux";
14
+ export {
15
+ isWindowCapable,
16
+ isSidebarCapable,
17
+ isBatchCapable,
18
+ isFullSidebarCapable,
19
+ } from "./contracts/mux";
20
+ export type { AgentStatus, AgentEvent } from "./contracts/agent";
21
+ export { TERMINAL_STATUSES } from "./contracts/agent";
22
+ export type { AgentWatcher, AgentWatcherContext } from "./contracts/agent-watcher";
23
+ export { AgentTracker } from "./agents/tracker";
24
+ export { AmpAgentWatcher } from "./agents/watchers/amp";
25
+ export { ClaudeCodeAgentWatcher } from "./agents/watchers/claude-code";
26
+ export { CodexAgentWatcher } from "./agents/watchers/codex";
27
+ export { OpenCodeAgentWatcher } from "./agents/watchers/opencode";
28
+ export { MuxRegistry } from "./mux/registry";
29
+ export { detectMux } from "./mux/detect";
30
+ export { PluginLoader } from "./plugins/loader";
31
+ export type { PluginAPI, PluginFactory } from "./plugins/loader";
32
+ export {
33
+ debugLog,
34
+ DEBUG_LOG,
35
+ TUI_RESIZE_LOG,
36
+ TUI_AGENT_CLICK_LOG,
37
+ SERVER_ERR_LOG,
38
+ INSTALL_LOG,
39
+ } from "./debug";
40
+ export { loadConfig, saveConfig } from "./config";
41
+ export type { AgentboardConfig } from "./config";
42
+ export { resolveTheme, BUILTIN_THEMES, DEFAULT_THEME } from "./themes";
43
+ export type { Theme, ThemePalette, PartialTheme } from "./themes";
44
+ export { startServer } from "./server/index";
45
+ export { ensureServer } from "./server/launcher";
46
+ export {
47
+ SERVER_PORT,
48
+ SERVER_HOST,
49
+ PID_FILE,
50
+ SERVER_IDLE_TIMEOUT_MS,
51
+ STUCK_RUNNING_TIMEOUT_MS,
52
+ C,
53
+ STATUS_COLORS,
54
+ STATUS_ICONS,
55
+ } from "./shared";
56
+ export type {
57
+ SessionData,
58
+ ServerState,
59
+ FocusUpdate,
60
+ ResizeNotify,
61
+ QuitNotify,
62
+ ServerMessage,
63
+ ClientCommand,
64
+ MetadataTone,
65
+ MetadataStatus,
66
+ MetadataProgress,
67
+ MetadataLogEntry,
68
+ SessionMetadata,
69
+ } from "./shared";
@@ -0,0 +1,20 @@
1
+ import type { MuxProvider } from "../contracts/mux";
2
+ import { MuxRegistry } from "./registry";
3
+
4
+ /**
5
+ * Auto-detect the terminal multiplexer from environment variables.
6
+ * Uses the registry to find matching providers.
7
+ *
8
+ * Detection: $TMUX → provider named "tmux"
9
+ *
10
+ * Users can override by passing their own MuxProvider.
11
+ */
12
+ export function detectMux(registry?: MuxRegistry): MuxProvider | null {
13
+ if (!registry) return null;
14
+
15
+ if (process.env.TMUX) {
16
+ return registry.get("tmux");
17
+ }
18
+
19
+ return null;
20
+ }
@@ -0,0 +1,45 @@
1
+ import type { MuxProvider } from "../contracts/mux";
2
+
3
+ /**
4
+ * Registry for MuxProvider implementations.
5
+ *
6
+ * The server resolves which provider to use via:
7
+ * 1. Explicit config override (user picks a mux by name)
8
+ * 2. Auto-detect from env ($TMUX)
9
+ * 3. First registered provider as fallback
10
+ */
11
+ export class MuxRegistry {
12
+ private providers = new Map<string, MuxProvider>();
13
+
14
+ register(provider: MuxProvider): void {
15
+ this.providers.set(provider.name, provider);
16
+ }
17
+
18
+ get(name: string): MuxProvider | null {
19
+ return this.providers.get(name) ?? null;
20
+ }
21
+
22
+ list(): string[] {
23
+ return [...this.providers.keys()];
24
+ }
25
+
26
+ /**
27
+ * Resolve the active MuxProvider.
28
+ * @param preference — explicit mux name from config. Takes priority.
29
+ * @returns the resolved provider, or null if none found.
30
+ */
31
+ resolve(preference?: string): MuxProvider | null {
32
+ // 1. Explicit preference
33
+ if (preference) {
34
+ return this.providers.get(preference) ?? null;
35
+ }
36
+
37
+ // 2. Auto-detect from environment (tmux only)
38
+ if (process.env.TMUX && this.providers.has("tmux")) {
39
+ return this.providers.get("tmux")!;
40
+ }
41
+
42
+ // 3. No match
43
+ return null;
44
+ }
45
+ }
@@ -0,0 +1,152 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { join, extname } from "node:path";
3
+ import type { MuxProvider } from "../contracts/mux";
4
+ import type { AgentWatcher } from "../contracts/agent-watcher";
5
+ import { MuxRegistry } from "../mux/registry";
6
+ import { SERVER_PORT, SERVER_HOST } from "../shared";
7
+
8
+ /**
9
+ * The API surface passed to every plugin factory function.
10
+ * Inspired by pi-mono's ExtensionAPI pattern:
11
+ * export default function(api: PluginAPI) { ... }
12
+ */
13
+ export interface PluginAPI {
14
+ registerMux(provider: MuxProvider): void;
15
+ registerWatcher(watcher: AgentWatcher): void;
16
+ readonly serverPort: number;
17
+ readonly serverHost: string;
18
+ }
19
+
20
+ /** Plugin factory — the single export a plugin must provide */
21
+ export type PluginFactory = (api: PluginAPI) => void | Promise<void>;
22
+
23
+ export class PluginLoader {
24
+ readonly registry = new MuxRegistry();
25
+ private watchers: AgentWatcher[] = [];
26
+
27
+ registerMux(provider: MuxProvider): void {
28
+ this.registry.register(provider);
29
+ }
30
+
31
+ registerWatcher(watcher: AgentWatcher): void {
32
+ this.watchers.push(watcher);
33
+ }
34
+
35
+ getWatchers(): AgentWatcher[] {
36
+ return [...this.watchers];
37
+ }
38
+
39
+ resolve(preference?: string): MuxProvider | null {
40
+ return this.registry.resolve(preference);
41
+ }
42
+
43
+ /**
44
+ * Build the PluginAPI object that gets passed to every factory function.
45
+ */
46
+ private createAPI(): PluginAPI {
47
+ return {
48
+ registerMux: (provider: MuxProvider) => this.registry.register(provider),
49
+ registerWatcher: (watcher: AgentWatcher) => this.registerWatcher(watcher),
50
+ serverPort: SERVER_PORT,
51
+ serverHost: SERVER_HOST,
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Load plugins from a directory (like ~/.config/towles-tool/agentboard/plugins/).
57
+ * Scans one level deep:
58
+ * - *.ts / *.js files → loaded directly
59
+ * - subdirs with index.ts / index.js → loaded as entry point
60
+ *
61
+ * Each must `export default function(api: PluginAPI) { ... }`
62
+ *
63
+ * Returns names of successfully loaded plugins.
64
+ */
65
+ loadDir(dir: string): string[] {
66
+ if (!existsSync(dir)) return [];
67
+
68
+ const loaded: string[] = [];
69
+ const api = this.createAPI();
70
+ const entries = readdirSync(dir);
71
+
72
+ for (const entry of entries) {
73
+ const fullPath = join(dir, entry);
74
+ const stat = statSync(fullPath);
75
+
76
+ if (stat.isFile()) {
77
+ const ext = extname(entry);
78
+ if (ext !== ".ts" && ext !== ".js") continue;
79
+ if (this.loadFactory(fullPath, api)) loaded.push(entry);
80
+ } else if (stat.isDirectory()) {
81
+ // Check for index.ts or index.js
82
+ const indexTs = join(fullPath, "index.ts");
83
+ const indexJs = join(fullPath, "index.js");
84
+ const indexPath = existsSync(indexTs) ? indexTs : existsSync(indexJs) ? indexJs : null;
85
+ if (indexPath && this.loadFactory(indexPath, api)) loaded.push(entry);
86
+ }
87
+ }
88
+
89
+ return loaded;
90
+ }
91
+
92
+ /**
93
+ * Load community plugins from npm package names.
94
+ * Each package should `export default function(api: PluginAPI) { ... }`
95
+ * or have a package.json "agentboard" field pointing to the entry file.
96
+ *
97
+ * Returns names of successfully loaded packages.
98
+ */
99
+ loadPackages(packageNames: string[]): string[] {
100
+ const loaded: string[] = [];
101
+ const api = this.createAPI();
102
+
103
+ for (const pkg of packageNames) {
104
+ try {
105
+ const mod = require(pkg);
106
+ const factory: PluginFactory | undefined =
107
+ typeof mod.default === "function"
108
+ ? mod.default
109
+ : typeof mod === "function"
110
+ ? mod
111
+ : undefined;
112
+ if (factory) {
113
+ factory(api);
114
+ loaded.push(pkg);
115
+ }
116
+ } catch {
117
+ // Package not installed or broken — skip
118
+ }
119
+ }
120
+
121
+ return loaded;
122
+ }
123
+
124
+ /**
125
+ * Load a single factory from a file path.
126
+ */
127
+ private loadFactory(filePath: string, api: PluginAPI): boolean {
128
+ try {
129
+ const mod = require(filePath);
130
+ const factory: PluginFactory | undefined =
131
+ typeof mod.default === "function"
132
+ ? mod.default
133
+ : typeof mod === "function"
134
+ ? mod
135
+ : undefined;
136
+ if (!factory) return false;
137
+ factory(api);
138
+ return true;
139
+ } catch {
140
+ return false;
141
+ }
142
+ }
143
+
144
+ getSetupInfo(): { registeredMuxProviders: string[]; configPath: string; serverPort: number } {
145
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
146
+ return {
147
+ registeredMuxProviders: this.registry.list(),
148
+ configPath: join(home, ".config", "towles-tool", "agentboard", "config.json"),
149
+ serverPort: SERVER_PORT,
150
+ };
151
+ }
152
+ }
@@ -0,0 +1,112 @@
1
+ import type { Server } from "bun";
2
+ import type { MuxProvider, FullSidebarCapable, SidebarPane } from "../contracts/mux";
3
+ import type { AgentTracker } from "../agents/tracker";
4
+ import type { SessionOrder } from "./session-order";
5
+ import type { SessionMetadataStore } from "./metadata-store";
6
+ import type { SidebarResizeContext, SidebarResizeSuppression } from "./sidebar-width-sync";
7
+ import type { ServerState } from "../shared";
8
+ import type { AgentStatus } from "../contracts/agent";
9
+
10
+ export interface PaneAgentPresence {
11
+ agent: string;
12
+ session: string;
13
+ paneId: string;
14
+ threadId?: string;
15
+ threadName?: string;
16
+ status?: AgentStatus;
17
+ lastSeenTs: number;
18
+ }
19
+
20
+ /**
21
+ * Shared mutable state for all server modules.
22
+ * Created once in startServer(), passed to all sub-modules.
23
+ */
24
+ export interface ServerContext {
25
+ // Core dependencies
26
+ mux: MuxProvider;
27
+ allProviders: MuxProvider[];
28
+ tracker: AgentTracker;
29
+ sessionOrder: SessionOrder;
30
+ metadataStore: SessionMetadataStore;
31
+
32
+ // Config
33
+ currentTheme: string | undefined;
34
+ sidebarWidth: number;
35
+ sidebarPosition: "left" | "right";
36
+ sidebarVisible: boolean;
37
+ home: string;
38
+
39
+ // Session/focus state
40
+ focusedSession: string | null;
41
+ lastState: ServerState | null;
42
+ clientCount: number;
43
+ idleTimer: ReturnType<typeof setTimeout> | null;
44
+ clientTtys: WeakMap<object, string>;
45
+ clientSessionNames: WeakMap<object, string>;
46
+ sessionProviders: Map<string, MuxProvider>;
47
+ clientTtyBySession: Map<string, string>;
48
+
49
+ // Current session cache
50
+ cachedCurrentSession: string | null;
51
+ cachedCurrentSessionTs: number;
52
+
53
+ // Sidebar state
54
+ pendingSidebarSpawns: Set<string>;
55
+ suppressedSidebarResizeAcks: Map<string, SidebarResizeSuppression>;
56
+ sidebarSnapshots: Map<string, { width?: number; windowWidth?: number }>;
57
+ pendingSidebarResize: ReturnType<typeof setTimeout> | null;
58
+ sidebarPaneCache: { provider: FullSidebarCapable; panes: SidebarPane[] }[] | null;
59
+ sidebarPaneCacheTs: number;
60
+ ensureSidebarTimer: ReturnType<typeof setTimeout> | null;
61
+ ensureSidebarPendingCtx: { session: string; windowId: string } | undefined;
62
+
63
+ // Pane agent scanning
64
+ paneAgentsBySession: Map<string, Map<string, PaneAgentPresence>>;
65
+ paneScanTimer: ReturnType<typeof setInterval> | null;
66
+
67
+ // Port polling
68
+ portPollTimer: ReturnType<typeof setInterval> | null;
69
+
70
+ // Highlight tracking
71
+ pendingHighlightResets: Map<string, ReturnType<typeof setTimeout>>;
72
+
73
+ // Server ref (set after Bun.serve)
74
+ server: Server;
75
+
76
+ // Wired functions
77
+ broadcastState: () => void;
78
+ broadcastFocusOnly: (sender?: any) => void;
79
+ getCurrentSession: () => string | null;
80
+ getCachedCurrentSession: () => string | null;
81
+ invalidateCurrentSessionCache: () => void;
82
+ getProvidersWithSidebar: () => FullSidebarCapable[];
83
+ listSidebarPanesByProvider: () => { provider: FullSidebarCapable; panes: SidebarPane[] }[];
84
+ invalidateSidebarPaneCache: () => void;
85
+ handleFocus: (name: string) => void;
86
+ moveFocus: (delta: -1 | 1, sender?: any) => void;
87
+ setFocus: (name: string, sender?: any) => void;
88
+ refreshPaneAgents: () => void;
89
+ cleanup: () => void;
90
+ toggleSidebar: (ctx?: { session: string; windowId: string }) => void;
91
+ ensureSidebarInWindow: (
92
+ provider?: FullSidebarCapable,
93
+ ctx?: { session: string; windowId: string },
94
+ ) => void;
95
+ debouncedEnsureSidebar: (ctx?: { session: string; windowId: string }) => void;
96
+ scheduleSidebarResize: (ctx?: SidebarResizeContext) => void;
97
+ resizeSidebars: (ctx?: SidebarResizeContext) => void;
98
+ quitAll: () => void;
99
+ switchToVisibleIndex: (index: number, clientTty?: string) => void;
100
+ focusAgentPane: (
101
+ sessionName: string,
102
+ agentName: string,
103
+ threadId?: string,
104
+ threadName?: string,
105
+ ) => void;
106
+ killAgentPane: (
107
+ sessionName: string,
108
+ agentName: string,
109
+ threadId?: string,
110
+ threadName?: string,
111
+ ) => void;
112
+ }