@towles/tool 0.0.107 → 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.
- package/README.md +7 -1
- package/package.json +2 -1
- package/plugins/tt-agentboard/README.md +160 -0
- package/plugins/tt-agentboard/apps/server/package.json +20 -0
- package/plugins/tt-agentboard/apps/server/src/main.ts +60 -0
- package/plugins/tt-agentboard/apps/tui/build.ts +11 -0
- package/plugins/tt-agentboard/apps/tui/bunfig.toml +1 -0
- package/plugins/tt-agentboard/apps/tui/package.json +23 -0
- package/plugins/tt-agentboard/apps/tui/scripts/sessionizer.sh +36 -0
- package/plugins/tt-agentboard/apps/tui/src/components/DetailPanel.tsx +350 -0
- package/plugins/tt-agentboard/apps/tui/src/components/DiffStats.tsx +33 -0
- package/plugins/tt-agentboard/apps/tui/src/components/SessionCard.tsx +177 -0
- package/plugins/tt-agentboard/apps/tui/src/components/StatusBar.tsx +49 -0
- package/plugins/tt-agentboard/apps/tui/src/constants.ts +46 -0
- package/plugins/tt-agentboard/apps/tui/src/detail-panel-height.ts +21 -0
- package/plugins/tt-agentboard/apps/tui/src/index.tsx +880 -0
- package/plugins/tt-agentboard/apps/tui/src/mux-context.ts +61 -0
- package/plugins/tt-agentboard/apps/tui/tsconfig.json +15 -0
- package/plugins/tt-agentboard/bun.lock +444 -0
- package/plugins/tt-agentboard/package.json +26 -0
- package/plugins/tt-agentboard/packages/mux-tmux/package.json +14 -0
- package/plugins/tt-agentboard/packages/mux-tmux/src/client.ts +550 -0
- package/plugins/tt-agentboard/packages/mux-tmux/src/index.ts +18 -0
- package/plugins/tt-agentboard/packages/mux-tmux/src/provider.ts +259 -0
- package/plugins/tt-agentboard/packages/mux-tmux/tsconfig.json +13 -0
- package/plugins/tt-agentboard/packages/runtime/package.json +14 -0
- package/plugins/tt-agentboard/packages/runtime/src/agents/tracker.ts +233 -0
- package/plugins/tt-agentboard/packages/runtime/src/agents/watchers/amp.ts +316 -0
- package/plugins/tt-agentboard/packages/runtime/src/agents/watchers/claude-code.ts +374 -0
- package/plugins/tt-agentboard/packages/runtime/src/agents/watchers/codex.ts +364 -0
- package/plugins/tt-agentboard/packages/runtime/src/agents/watchers/opencode.ts +249 -0
- package/plugins/tt-agentboard/packages/runtime/src/config.ts +70 -0
- package/plugins/tt-agentboard/packages/runtime/src/contracts/agent-watcher.ts +38 -0
- package/plugins/tt-agentboard/packages/runtime/src/contracts/agent.ts +16 -0
- package/plugins/tt-agentboard/packages/runtime/src/contracts/index.ts +3 -0
- package/plugins/tt-agentboard/packages/runtime/src/contracts/mux.ts +148 -0
- package/plugins/tt-agentboard/packages/runtime/src/debug.ts +19 -0
- package/plugins/tt-agentboard/packages/runtime/src/index.ts +69 -0
- package/plugins/tt-agentboard/packages/runtime/src/mux/detect.ts +20 -0
- package/plugins/tt-agentboard/packages/runtime/src/mux/registry.ts +45 -0
- package/plugins/tt-agentboard/packages/runtime/src/plugins/loader.ts +152 -0
- package/plugins/tt-agentboard/packages/runtime/src/server/context.ts +112 -0
- package/plugins/tt-agentboard/packages/runtime/src/server/git-info.ts +164 -0
- package/plugins/tt-agentboard/packages/runtime/src/server/index.ts +1753 -0
- package/plugins/tt-agentboard/packages/runtime/src/server/launcher.ts +71 -0
- package/plugins/tt-agentboard/packages/runtime/src/server/metadata-store.ts +86 -0
- package/plugins/tt-agentboard/packages/runtime/src/server/pane-scanner.ts +327 -0
- package/plugins/tt-agentboard/packages/runtime/src/server/port-scanner.ts +155 -0
- package/plugins/tt-agentboard/packages/runtime/src/server/session-order.ts +127 -0
- package/plugins/tt-agentboard/packages/runtime/src/server/sidebar-manager.ts +232 -0
- package/plugins/tt-agentboard/packages/runtime/src/server/sidebar-width-sync.ts +66 -0
- package/plugins/tt-agentboard/packages/runtime/src/shared.ts +179 -0
- package/plugins/tt-agentboard/packages/runtime/src/themes.ts +750 -0
- package/plugins/tt-agentboard/packages/runtime/test/config.test.ts +83 -0
- package/plugins/tt-agentboard/packages/runtime/test/tracker.test.ts +172 -0
- package/plugins/tt-agentboard/packages/runtime/tsconfig.json +13 -0
- package/plugins/tt-agentboard/tsconfig.json +19 -0
- package/plugins/tt-auto-claude/.claude-plugin/plugin.json +8 -0
- package/plugins/tt-auto-claude/commands/create-issue.md +20 -0
- package/plugins/tt-auto-claude/commands/list.md +21 -0
- package/plugins/tt-auto-claude/skills/auto-claude/SKILL.md +71 -0
- package/plugins/tt-core/.claude-plugin/plugin.json +8 -0
- package/plugins/tt-core/README.md +18 -0
- package/plugins/tt-core/commands/improve-architecture.md +66 -0
- package/plugins/tt-core/commands/interview-me.md +38 -0
- package/plugins/tt-core/commands/prd-to-issues.md +49 -0
- package/plugins/tt-core/commands/refine-text.md +30 -0
- package/plugins/tt-core/commands/task.md +37 -0
- package/plugins/tt-core/commands/tdd.md +69 -0
- package/plugins/tt-core/commands/write-prd.md +69 -0
- package/plugins/tt-core/promptfooconfig.interview-me.yaml +155 -0
- package/plugins/tt-core/promptfooconfig.refine-text.yaml +242 -0
- package/plugins/tt-core/promptfooconfig.tdd.yaml +144 -0
- package/plugins/tt-core/promptfooconfig.write-prd.yaml +145 -0
- package/plugins/tt-core/skills/towles-tool/SKILL.md +35 -0
- 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,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
|
+
}
|