aegis-bridge 0.1.0

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 (137) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +404 -0
  3. package/dashboard/dist/assets/index-BoZwGLAx.css +32 -0
  4. package/dashboard/dist/assets/index-C61BkKH-.js +312 -0
  5. package/dashboard/dist/assets/index-C61BkKH-.js.map +1 -0
  6. package/dashboard/dist/index.html +14 -0
  7. package/dist/api-contracts.d.ts +229 -0
  8. package/dist/api-contracts.js +7 -0
  9. package/dist/api-contracts.typecheck.d.ts +14 -0
  10. package/dist/api-contracts.typecheck.js +1 -0
  11. package/dist/api-error-envelope.d.ts +15 -0
  12. package/dist/api-error-envelope.js +80 -0
  13. package/dist/auth.d.ts +87 -0
  14. package/dist/auth.js +276 -0
  15. package/dist/channels/index.d.ts +8 -0
  16. package/dist/channels/index.js +8 -0
  17. package/dist/channels/manager.d.ts +47 -0
  18. package/dist/channels/manager.js +115 -0
  19. package/dist/channels/telegram-style.d.ts +118 -0
  20. package/dist/channels/telegram-style.js +202 -0
  21. package/dist/channels/telegram.d.ts +91 -0
  22. package/dist/channels/telegram.js +1518 -0
  23. package/dist/channels/types.d.ts +77 -0
  24. package/dist/channels/types.js +8 -0
  25. package/dist/channels/webhook.d.ts +60 -0
  26. package/dist/channels/webhook.js +216 -0
  27. package/dist/cli.d.ts +8 -0
  28. package/dist/cli.js +252 -0
  29. package/dist/config.d.ts +90 -0
  30. package/dist/config.js +214 -0
  31. package/dist/consensus.d.ts +16 -0
  32. package/dist/consensus.js +19 -0
  33. package/dist/continuation-pointer.d.ts +11 -0
  34. package/dist/continuation-pointer.js +65 -0
  35. package/dist/diagnostics.d.ts +27 -0
  36. package/dist/diagnostics.js +95 -0
  37. package/dist/error-categories.d.ts +39 -0
  38. package/dist/error-categories.js +73 -0
  39. package/dist/events.d.ts +133 -0
  40. package/dist/events.js +389 -0
  41. package/dist/fault-injection.d.ts +29 -0
  42. package/dist/fault-injection.js +115 -0
  43. package/dist/file-utils.d.ts +2 -0
  44. package/dist/file-utils.js +37 -0
  45. package/dist/handshake.d.ts +60 -0
  46. package/dist/handshake.js +124 -0
  47. package/dist/hook-settings.d.ts +80 -0
  48. package/dist/hook-settings.js +272 -0
  49. package/dist/hook.d.ts +19 -0
  50. package/dist/hook.js +231 -0
  51. package/dist/hooks.d.ts +32 -0
  52. package/dist/hooks.js +364 -0
  53. package/dist/jsonl-watcher.d.ts +59 -0
  54. package/dist/jsonl-watcher.js +166 -0
  55. package/dist/logger.d.ts +35 -0
  56. package/dist/logger.js +65 -0
  57. package/dist/mcp-server.d.ts +123 -0
  58. package/dist/mcp-server.js +869 -0
  59. package/dist/memory-bridge.d.ts +27 -0
  60. package/dist/memory-bridge.js +137 -0
  61. package/dist/memory-routes.d.ts +3 -0
  62. package/dist/memory-routes.js +100 -0
  63. package/dist/metrics.d.ts +126 -0
  64. package/dist/metrics.js +286 -0
  65. package/dist/model-router.d.ts +53 -0
  66. package/dist/model-router.js +150 -0
  67. package/dist/monitor.d.ts +103 -0
  68. package/dist/monitor.js +820 -0
  69. package/dist/path-utils.d.ts +11 -0
  70. package/dist/path-utils.js +21 -0
  71. package/dist/permission-evaluator.d.ts +10 -0
  72. package/dist/permission-evaluator.js +48 -0
  73. package/dist/permission-guard.d.ts +51 -0
  74. package/dist/permission-guard.js +196 -0
  75. package/dist/permission-request-manager.d.ts +12 -0
  76. package/dist/permission-request-manager.js +36 -0
  77. package/dist/permission-routes.d.ts +7 -0
  78. package/dist/permission-routes.js +28 -0
  79. package/dist/pipeline.d.ts +97 -0
  80. package/dist/pipeline.js +291 -0
  81. package/dist/process-utils.d.ts +4 -0
  82. package/dist/process-utils.js +73 -0
  83. package/dist/question-manager.d.ts +54 -0
  84. package/dist/question-manager.js +80 -0
  85. package/dist/retry.d.ts +11 -0
  86. package/dist/retry.js +34 -0
  87. package/dist/safe-json.d.ts +12 -0
  88. package/dist/safe-json.js +22 -0
  89. package/dist/screenshot.d.ts +28 -0
  90. package/dist/screenshot.js +60 -0
  91. package/dist/server.d.ts +10 -0
  92. package/dist/server.js +1973 -0
  93. package/dist/session-cleanup.d.ts +18 -0
  94. package/dist/session-cleanup.js +11 -0
  95. package/dist/session.d.ts +379 -0
  96. package/dist/session.js +1568 -0
  97. package/dist/shutdown-utils.d.ts +5 -0
  98. package/dist/shutdown-utils.js +24 -0
  99. package/dist/signal-cleanup-helper.d.ts +48 -0
  100. package/dist/signal-cleanup-helper.js +117 -0
  101. package/dist/sse-limiter.d.ts +47 -0
  102. package/dist/sse-limiter.js +61 -0
  103. package/dist/sse-writer.d.ts +31 -0
  104. package/dist/sse-writer.js +94 -0
  105. package/dist/ssrf.d.ts +102 -0
  106. package/dist/ssrf.js +267 -0
  107. package/dist/startup.d.ts +6 -0
  108. package/dist/startup.js +162 -0
  109. package/dist/suppress.d.ts +33 -0
  110. package/dist/suppress.js +79 -0
  111. package/dist/swarm-monitor.d.ts +117 -0
  112. package/dist/swarm-monitor.js +300 -0
  113. package/dist/template-store.d.ts +45 -0
  114. package/dist/template-store.js +142 -0
  115. package/dist/terminal-parser.d.ts +16 -0
  116. package/dist/terminal-parser.js +346 -0
  117. package/dist/tmux-capture-cache.d.ts +18 -0
  118. package/dist/tmux-capture-cache.js +34 -0
  119. package/dist/tmux.d.ts +183 -0
  120. package/dist/tmux.js +906 -0
  121. package/dist/tool-registry.d.ts +40 -0
  122. package/dist/tool-registry.js +83 -0
  123. package/dist/transcript.d.ts +63 -0
  124. package/dist/transcript.js +284 -0
  125. package/dist/utils/circular-buffer.d.ts +11 -0
  126. package/dist/utils/circular-buffer.js +37 -0
  127. package/dist/utils/redact-headers.d.ts +13 -0
  128. package/dist/utils/redact-headers.js +54 -0
  129. package/dist/validation.d.ts +406 -0
  130. package/dist/validation.js +415 -0
  131. package/dist/verification.d.ts +2 -0
  132. package/dist/verification.js +72 -0
  133. package/dist/worktree-lookup.d.ts +24 -0
  134. package/dist/worktree-lookup.js +71 -0
  135. package/dist/ws-terminal.d.ts +32 -0
  136. package/dist/ws-terminal.js +348 -0
  137. package/package.json +83 -0
@@ -0,0 +1,300 @@
1
+ /**
2
+ * swarm-monitor.ts — Monitors Claude Code swarm sockets for teammate sessions.
3
+ *
4
+ * Issue #81: Agent Swarm Awareness.
5
+ *
6
+ * When CC spawns teammates/subagents, it creates them in tmux with:
7
+ * - Socket: -L claude-swarm-{pid} (isolated from main session)
8
+ * - Window naming: teammate-{name}
9
+ * - Env vars: CLAUDE_PARENT_SESSION_ID, --agent-id, --agent-name
10
+ *
11
+ * This module discovers those swarm sockets, lists their windows,
12
+ * cross-references with parent sessions, and tracks teammate status.
13
+ */
14
+ import { execFile } from 'node:child_process';
15
+ import { promisify } from 'node:util';
16
+ import { readdir } from 'node:fs/promises';
17
+ import { tmpdir } from 'node:os';
18
+ const execFileAsync = promisify(execFile);
19
+ const TMUX_TIMEOUT_MS = 5_000;
20
+ export const DEFAULT_SWARM_CONFIG = {
21
+ scanIntervalMs: 10_000,
22
+ socketGlobPattern: 'tmux-claude-swarm-*',
23
+ };
24
+ export class SwarmMonitor {
25
+ sessions;
26
+ config;
27
+ running = false;
28
+ lastResult = null;
29
+ timer = null;
30
+ eventHandlers = [];
31
+ windowsDisabledLogged = false;
32
+ constructor(sessions, config = DEFAULT_SWARM_CONFIG) {
33
+ this.sessions = sessions;
34
+ this.config = config;
35
+ }
36
+ /** Register an event handler for teammate lifecycle events. */
37
+ onEvent(handler) {
38
+ this.eventHandlers.push(handler);
39
+ }
40
+ emitEvent(event) {
41
+ for (const handler of this.eventHandlers) {
42
+ try {
43
+ handler(event);
44
+ }
45
+ catch (e) {
46
+ console.error('SwarmMonitor event handler error:', e);
47
+ }
48
+ }
49
+ }
50
+ isWindowsPlatform() {
51
+ return process.platform === 'win32';
52
+ }
53
+ logWindowsDisabled() {
54
+ if (this.windowsDisabledLogged)
55
+ return;
56
+ console.info('SwarmMonitor disabled on Windows: tmux swarm sockets are not supported on this platform.');
57
+ this.windowsDisabledLogged = true;
58
+ }
59
+ /** Start the periodic scan loop. */
60
+ start() {
61
+ if (this.isWindowsPlatform()) {
62
+ this.logWindowsDisabled();
63
+ return;
64
+ }
65
+ if (this.running)
66
+ return;
67
+ this.running = true;
68
+ void this.scan();
69
+ this.timer = setInterval(() => {
70
+ void this.scan();
71
+ }, this.config.scanIntervalMs);
72
+ }
73
+ /** Stop the periodic scan loop. */
74
+ stop() {
75
+ this.running = false;
76
+ if (this.timer) {
77
+ clearInterval(this.timer);
78
+ this.timer = null;
79
+ }
80
+ }
81
+ /** Get the most recent scan result. */
82
+ getLastResult() {
83
+ return this.lastResult;
84
+ }
85
+ /** Run a single scan and return the result. */
86
+ async scan() {
87
+ if (this.isWindowsPlatform()) {
88
+ this.logWindowsDisabled();
89
+ this.lastResult = {
90
+ swarms: [],
91
+ totalSockets: 0,
92
+ totalTeammates: 0,
93
+ scannedAt: Date.now(),
94
+ };
95
+ return this.lastResult;
96
+ }
97
+ try {
98
+ const sockets = await this.discoverSwarmSockets();
99
+ // Issue #353: Inspect sockets in parallel to avoid N×timeout accumulation.
100
+ const results = await Promise.allSettled(sockets.map(socketName => this.inspectSwarmSocket(socketName)));
101
+ const swarms = [];
102
+ for (const result of results) {
103
+ if (result.status === 'fulfilled') {
104
+ swarms.push(result.value);
105
+ }
106
+ }
107
+ this.lastResult = {
108
+ swarms,
109
+ totalSockets: sockets.length,
110
+ totalTeammates: swarms.reduce((sum, s) => sum + s.teammates.length, 0),
111
+ scannedAt: Date.now(),
112
+ };
113
+ this.detectChanges();
114
+ return this.lastResult;
115
+ }
116
+ catch (e) {
117
+ // Issue #353: Prevent unhandled rejection from setInterval fire-and-forget.
118
+ console.error('SwarmMonitor scan error:', e);
119
+ return this.lastResult ?? {
120
+ swarms: [],
121
+ totalSockets: 0,
122
+ totalTeammates: 0,
123
+ scannedAt: Date.now(),
124
+ };
125
+ }
126
+ }
127
+ /** Compare current scan result against previous to detect teammate changes. */
128
+ detectChanges() {
129
+ if (!this.lastResult)
130
+ return;
131
+ for (const swarm of this.lastResult.swarms) {
132
+ // Issue #353: Always update previous snapshot, even without a parent session,
133
+ // to prevent repeated spawn events on every scan cycle.
134
+ const prevSwarm = this.previousTeammates.get(swarm.socketName);
135
+ this.previousTeammates.set(swarm.socketName, swarm.teammates.map(t => ({ ...t })));
136
+ if (!swarm.parentSession)
137
+ continue;
138
+ const prevNames = new Set(prevSwarm?.map(t => t.windowName) ?? []);
139
+ // New teammates
140
+ for (const teammate of swarm.teammates) {
141
+ if (!prevNames.has(teammate.windowName) && teammate.status !== 'dead') {
142
+ this.emitEvent({ type: 'teammate_spawned', swarm, teammate });
143
+ }
144
+ }
145
+ // Finished teammates (previously seen, now dead)
146
+ if (prevSwarm) {
147
+ for (const prev of prevSwarm) {
148
+ const current = swarm.teammates.find(t => t.windowName === prev.windowName);
149
+ if (!current) {
150
+ // Teammate window gone entirely
151
+ this.emitEvent({ type: 'teammate_finished', swarm, teammate: { ...prev, status: 'dead', alive: false } });
152
+ }
153
+ else if (prev.status === 'running' && current.status === 'dead') {
154
+ this.emitEvent({ type: 'teammate_finished', swarm, teammate: current });
155
+ }
156
+ }
157
+ }
158
+ }
159
+ // Clean up stale socket tracking
160
+ for (const socketName of this.previousTeammates.keys()) {
161
+ if (!this.lastResult.swarms.find(s => s.socketName === socketName)) {
162
+ this.previousTeammates.delete(socketName);
163
+ }
164
+ }
165
+ }
166
+ /** Snapshot of teammates from previous scan for diffing. */
167
+ previousTeammates = new Map();
168
+ /** Cached /tmp listing to avoid redundant I/O on every scan. */
169
+ cachedSocketNames = [];
170
+ cachedSocketAt = 0;
171
+ static SOCKET_CACHE_TTL_MS = 5_000;
172
+ /** Discover swarm socket directories in /tmp. */
173
+ async discoverSwarmSockets() {
174
+ try {
175
+ // Issue #353: Cache /tmp listing for 5s to avoid redundant I/O.
176
+ const now = Date.now();
177
+ if (this.cachedSocketNames.length > 0 && now - this.cachedSocketAt < SwarmMonitor.SOCKET_CACHE_TTL_MS) {
178
+ return this.cachedSocketNames;
179
+ }
180
+ const entries = await readdir(tmpdir());
181
+ // Match "tmux-<socketName>" directories (tmux socket dirs start with "tmux-")
182
+ const socketNames = [];
183
+ for (const entry of entries) {
184
+ if (entry.startsWith('tmux-')) {
185
+ // Extract socket name: tmux-<socketName> → <socketName>
186
+ const socketName = entry.slice(5); // remove "tmux-"
187
+ // Verify it's a claude-swarm-* socket
188
+ if (socketName.startsWith('claude-swarm-')) {
189
+ socketNames.push(socketName);
190
+ }
191
+ }
192
+ }
193
+ this.cachedSocketNames = socketNames;
194
+ this.cachedSocketAt = now;
195
+ return socketNames;
196
+ }
197
+ catch { /* tmux list-sockets failed — no swarm sockets visible */
198
+ return [];
199
+ }
200
+ }
201
+ /** Inspect a single swarm socket and return swarm info. */
202
+ async inspectSwarmSocket(socketName) {
203
+ if (this.isWindowsPlatform()) {
204
+ const pid = this.extractPid(socketName);
205
+ return {
206
+ socketName,
207
+ pid,
208
+ parentSession: null,
209
+ teammates: [],
210
+ aggregatedStatus: 'no_teammates',
211
+ lastScannedAt: Date.now(),
212
+ };
213
+ }
214
+ const pid = this.extractPid(socketName);
215
+ const teammates = await this.listSwarmWindows(socketName);
216
+ const parentSession = this.findParentSession(pid, teammates);
217
+ const aggregatedStatus = this.computeAggregatedStatus(teammates);
218
+ return {
219
+ socketName,
220
+ pid,
221
+ parentSession,
222
+ teammates,
223
+ aggregatedStatus,
224
+ lastScannedAt: Date.now(),
225
+ };
226
+ }
227
+ /** Extract PID from socket name "claude-swarm-{pid}". */
228
+ extractPid(socketName) {
229
+ const match = socketName.match(/^claude-swarm-(\d+)$/);
230
+ return match ? parseInt(match[1], 10) : 0;
231
+ }
232
+ /** List all windows in a swarm socket. */
233
+ async listSwarmWindows(socketName) {
234
+ try {
235
+ const { stdout } = await execFileAsync('tmux', ['-L', socketName, 'list-windows', '-F', '#{window_id}\t#{window_name}\t#{pane_current_path}\t#{pane_current_command}'], { timeout: TMUX_TIMEOUT_MS });
236
+ const raw = stdout.trim();
237
+ if (!raw)
238
+ return [];
239
+ return raw.split('\n').filter(Boolean).map(line => {
240
+ const [windowId, windowName, cwd, paneCommand] = line.split('\t');
241
+ const cmd = (paneCommand || '').toLowerCase();
242
+ const alive = cmd === 'claude' || cmd === 'node';
243
+ const status = alive
244
+ ? 'running'
245
+ : (paneCommand === '' || paneCommand === 'bash' || paneCommand === 'zsh')
246
+ ? 'idle'
247
+ : 'dead';
248
+ return {
249
+ windowId: windowId || '',
250
+ windowName: windowName || '',
251
+ cwd: cwd || '',
252
+ paneCommand: paneCommand || '',
253
+ alive,
254
+ status,
255
+ };
256
+ });
257
+ }
258
+ catch {
259
+ // Socket may be stale (parent process died)
260
+ return [];
261
+ }
262
+ }
263
+ /** Find the parent Aegis session for a swarm by matching the CC process PID. */
264
+ findParentSession(pid, _teammates) {
265
+ if (pid === 0)
266
+ return null;
267
+ // Issue #353: Match swarm socket PID against session.ccPid.
268
+ // The swarm socket name (claude-swarm-{pid}) contains the PID of the parent CC process.
269
+ for (const session of this.sessions.listSessions()) {
270
+ if (session.ccPid === pid) {
271
+ return session;
272
+ }
273
+ }
274
+ return null;
275
+ }
276
+ /** Compute aggregated status for a swarm. */
277
+ computeAggregatedStatus(teammates) {
278
+ if (teammates.length === 0)
279
+ return 'no_teammates';
280
+ const allDead = teammates.every(t => t.status === 'dead');
281
+ if (allDead)
282
+ return 'all_dead';
283
+ const anyWorking = teammates.some(t => t.status === 'running');
284
+ if (anyWorking)
285
+ return 'some_working';
286
+ return 'all_idle';
287
+ }
288
+ /** Find a specific swarm by parent session ID. */
289
+ findSwarmByParentSessionId(sessionId) {
290
+ if (!this.lastResult)
291
+ return null;
292
+ return this.lastResult.swarms.find(s => s.parentSession?.id === sessionId) ?? null;
293
+ }
294
+ /** Find all swarms associated with any active session. */
295
+ findActiveSwarms() {
296
+ if (!this.lastResult)
297
+ return [];
298
+ return this.lastResult.swarms.filter(s => s.parentSession !== null && s.teammates.length > 0);
299
+ }
300
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * template-store.ts — Session template persistence.
3
+ *
4
+ * Manages saving, loading, and listing session templates.
5
+ * Templates are stored in ~/.config/aegis/templates.json
6
+ */
7
+ export interface SessionTemplate {
8
+ id: string;
9
+ name: string;
10
+ description?: string;
11
+ workDir: string;
12
+ prompt?: string;
13
+ claudeCommand?: string;
14
+ env?: Record<string, string>;
15
+ stallThresholdMs?: number;
16
+ permissionMode?: 'default' | 'bypassPermissions' | 'plan' | 'acceptEdits' | 'dontAsk' | 'auto';
17
+ autoApprove?: boolean;
18
+ parentId?: string;
19
+ memoryKeys?: string[];
20
+ createdAt: number;
21
+ updatedAt: number;
22
+ }
23
+ export interface TemplateStore {
24
+ templates: Record<string, SessionTemplate>;
25
+ }
26
+ /**
27
+ * Create a new template from session parameters.
28
+ */
29
+ export declare function createTemplate(input: Omit<SessionTemplate, 'id' | 'createdAt' | 'updatedAt'>): Promise<SessionTemplate>;
30
+ /**
31
+ * Get a template by ID.
32
+ */
33
+ export declare function getTemplate(id: string): Promise<SessionTemplate | null>;
34
+ /**
35
+ * List all templates.
36
+ */
37
+ export declare function listTemplates(): Promise<SessionTemplate[]>;
38
+ /**
39
+ * Update a template.
40
+ */
41
+ export declare function updateTemplate(id: string, updates: Partial<Omit<SessionTemplate, 'id' | 'createdAt'>>): Promise<SessionTemplate | null>;
42
+ /**
43
+ * Delete a template.
44
+ */
45
+ export declare function deleteTemplate(id: string): Promise<boolean>;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * template-store.ts — Session template persistence.
3
+ *
4
+ * Manages saving, loading, and listing session templates.
5
+ * Templates are stored in ~/.config/aegis/templates.json
6
+ */
7
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
8
+ import { existsSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { homedir } from 'node:os';
11
+ import { randomUUID } from 'node:crypto';
12
+ import { safeJsonParse } from './safe-json.js';
13
+ function isSessionTemplate(value) {
14
+ if (!value || typeof value !== 'object' || Array.isArray(value))
15
+ return false;
16
+ const t = value;
17
+ return (typeof t.id === 'string' &&
18
+ typeof t.name === 'string' &&
19
+ typeof t.workDir === 'string' &&
20
+ typeof t.createdAt === 'number' &&
21
+ typeof t.updatedAt === 'number');
22
+ }
23
+ function isTemplateStore(value) {
24
+ if (!value || typeof value !== 'object' || Array.isArray(value))
25
+ return false;
26
+ const record = value;
27
+ if (!record.templates || typeof record.templates !== 'object' || Array.isArray(record.templates))
28
+ return false;
29
+ for (const tmpl of Object.values(record.templates)) {
30
+ if (!isSessionTemplate(tmpl))
31
+ return false;
32
+ }
33
+ return true;
34
+ }
35
+ const CONFIG_DIR = join(homedir(), '.config', 'aegis');
36
+ const TEMPLATES_FILE = join(CONFIG_DIR, 'templates.json');
37
+ let cachedTemplates = {};
38
+ let loaded = false;
39
+ /**
40
+ * Load templates from disk, or initialize empty store.
41
+ */
42
+ async function loadTemplates() {
43
+ if (loaded)
44
+ return cachedTemplates;
45
+ try {
46
+ if (existsSync(TEMPLATES_FILE)) {
47
+ const content = await readFile(TEMPLATES_FILE, 'utf-8');
48
+ const parsed = safeJsonParse(content, 'templates.json');
49
+ if (!parsed.ok || !isTemplateStore(parsed.data)) {
50
+ console.warn(`Failed to parse templates store: ${parsed.ok ? 'invalid structure' : parsed.error}`);
51
+ cachedTemplates = {};
52
+ }
53
+ else {
54
+ cachedTemplates = parsed.data.templates || {};
55
+ }
56
+ }
57
+ else {
58
+ cachedTemplates = {};
59
+ }
60
+ }
61
+ catch (err) {
62
+ console.error(`Failed to load templates from ${TEMPLATES_FILE}:`, err);
63
+ cachedTemplates = {};
64
+ }
65
+ loaded = true;
66
+ return cachedTemplates;
67
+ }
68
+ /**
69
+ * Persist templates to disk.
70
+ */
71
+ async function saveTemplates() {
72
+ try {
73
+ if (!existsSync(CONFIG_DIR)) {
74
+ await mkdir(CONFIG_DIR, { recursive: true });
75
+ }
76
+ const store = { templates: cachedTemplates };
77
+ await writeFile(TEMPLATES_FILE, JSON.stringify(store, null, 2));
78
+ }
79
+ catch (err) {
80
+ console.error(`Failed to save templates to ${TEMPLATES_FILE}:`, err);
81
+ throw err;
82
+ }
83
+ }
84
+ /**
85
+ * Create a new template from session parameters.
86
+ */
87
+ export async function createTemplate(input) {
88
+ await loadTemplates();
89
+ const template = {
90
+ ...input,
91
+ id: randomUUID(),
92
+ createdAt: Date.now(),
93
+ updatedAt: Date.now(),
94
+ };
95
+ cachedTemplates[template.id] = template;
96
+ await saveTemplates();
97
+ return template;
98
+ }
99
+ /**
100
+ * Get a template by ID.
101
+ */
102
+ export async function getTemplate(id) {
103
+ await loadTemplates();
104
+ return cachedTemplates[id] ?? null;
105
+ }
106
+ /**
107
+ * List all templates.
108
+ */
109
+ export async function listTemplates() {
110
+ await loadTemplates();
111
+ return Object.values(cachedTemplates).sort((a, b) => b.updatedAt - a.updatedAt);
112
+ }
113
+ /**
114
+ * Update a template.
115
+ */
116
+ export async function updateTemplate(id, updates) {
117
+ await loadTemplates();
118
+ const template = cachedTemplates[id];
119
+ if (!template)
120
+ return null;
121
+ const updated = {
122
+ ...template,
123
+ ...updates,
124
+ id: template.id,
125
+ createdAt: template.createdAt,
126
+ updatedAt: Date.now(),
127
+ };
128
+ cachedTemplates[id] = updated;
129
+ await saveTemplates();
130
+ return updated;
131
+ }
132
+ /**
133
+ * Delete a template.
134
+ */
135
+ export async function deleteTemplate(id) {
136
+ await loadTemplates();
137
+ if (!cachedTemplates[id])
138
+ return false;
139
+ delete cachedTemplates[id];
140
+ await saveTemplates();
141
+ return true;
142
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * terminal-parser.ts — Detects Claude Code UI state from tmux pane content.
3
+ *
4
+ * Port of CCBot's terminal_parser.py.
5
+ * Detects: permission prompts, plan mode, ask questions, status line.
6
+ */
7
+ export type UIState = 'idle' | 'working' | 'compacting' | 'context_warning' | 'waiting_for_input' | 'permission_prompt' | 'plan_mode' | 'ask_question' | 'bash_approval' | 'settings' | 'error' | 'unknown';
8
+ /** Detect the UI state from captured pane text. */
9
+ export declare function detectUIState(paneText: string): UIState;
10
+ /** Extract the interactive UI content if present. */
11
+ export declare function extractInteractiveContent(paneText: string): {
12
+ content: string;
13
+ name: UIState;
14
+ } | null;
15
+ /** Parse the status line text (what CC is doing). */
16
+ export declare function parseStatusLine(paneText: string): string | null;