@dinhtungdu/watcher 1.0.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.
@@ -0,0 +1,336 @@
1
+ import { AgentActivityItem, AgentPane, AgentStatus, GitMetadata, ObservationCapability, TerminalTarget } from './model.js';
2
+ import { WatcherAgentEventInput, WatcherAgentEventPayload } from './agentEvents.js';
3
+ import { canonicalSurfaceKey } from './surfaceIdentity.js';
4
+
5
+ export interface AgentEventContext {
6
+ target: TerminalTarget;
7
+ cwd?: string;
8
+ git?: GitMetadata;
9
+ now: number;
10
+ }
11
+
12
+ export function mapAgentEventToStatus(type: WatcherAgentEventInput['type']): AgentStatus {
13
+ switch (type) {
14
+ case 'session-started':
15
+ return 'unknown';
16
+ case 'agent-started':
17
+ return 'working';
18
+ case 'user-message':
19
+ case 'assistant-delta':
20
+ case 'assistant-message':
21
+ case 'tool-started':
22
+ case 'tool-updated':
23
+ case 'tool-finished':
24
+ return 'working';
25
+ case 'needs-input':
26
+ case 'error':
27
+ return 'needs_input';
28
+ case 'agent-finished':
29
+ return 'idle';
30
+ }
31
+ }
32
+
33
+ function summarize(value: string | undefined, fallback: string): string {
34
+ if (!value) return fallback;
35
+ const oneLine = value.replace(/\s+/g, ' ').trim();
36
+ return oneLine.length > 140 ? `${oneLine.slice(0, 139)}…` : oneLine;
37
+ }
38
+
39
+ function isPlaceholderSummary(value: string | undefined): boolean {
40
+ return !value
41
+ || value === 'Waiting for first task'
42
+ || value === 'Finished'
43
+ || value.startsWith('Detected ');
44
+ }
45
+
46
+ function eventObservation(previous: AgentPane | undefined, type: WatcherAgentEventInput['type']): ObservationCapability {
47
+ const terminalPreview = previous?.observation?.terminalPreview ?? false;
48
+ const assistantDeltas = (previous?.observation?.assistantDeltas ?? false) || type === 'assistant-delta';
49
+ return {
50
+ source: terminalPreview ? 'mixed' : 'event-source',
51
+ semanticEvents: true,
52
+ assistantDeltas,
53
+ terminalPreview,
54
+ };
55
+ }
56
+
57
+ function basePane(previous: AgentPane | undefined, input: WatcherAgentEventInput, context: AgentEventContext, status: AgentStatus): AgentPane {
58
+ return {
59
+ ...previous,
60
+ id: canonicalSurfaceKey(input.surface),
61
+ agentType: input.agent,
62
+ status,
63
+ reportedStatus: status === 'stalled' ? 'working' : status,
64
+ summary: previous?.summary ?? 'Waiting for first task',
65
+ target: context.target,
66
+ cwd: context.cwd,
67
+ git: context.git,
68
+ updatedAt: input.now ?? context.now,
69
+ observation: eventObservation(previous, input.type),
70
+ };
71
+ }
72
+
73
+ function assistantActivityId(payload: WatcherAgentEventPayload): string {
74
+ return `assistant:${payload.messageId ?? 'current'}`;
75
+ }
76
+
77
+ function toolActivityId(payload: WatcherAgentEventPayload): string | undefined {
78
+ return payload.id ? `tool:${payload.id}` : undefined;
79
+ }
80
+
81
+ function mergeActivityItems(previous: AgentActivityItem[] | undefined, updates: AgentActivityItem[], maxItems = 3): AgentActivityItem[] | undefined {
82
+ if (!previous?.length && updates.length === 0) return undefined;
83
+ const byId = new Map<string, AgentActivityItem>();
84
+ for (const item of previous ?? []) byId.set(item.id, item);
85
+ for (const item of updates) {
86
+ const existing = byId.get(item.id);
87
+ byId.set(item.id, existing ? { ...existing, ...item } : item);
88
+ }
89
+ const items = [...byId.values()].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, maxItems);
90
+ return items.length > 0 ? items : undefined;
91
+ }
92
+
93
+ function summaryAfterNonUserEvent(previous: AgentPane | undefined, fallbackText: string | undefined, fallbackSummary: string): string {
94
+ if (previous?.userMessage) return previous.summary;
95
+ if (previous?.summary && !isPlaceholderSummary(previous.summary) && !fallbackText) return previous.summary;
96
+ return summarize(fallbackText, fallbackSummary);
97
+ }
98
+
99
+ function nonEmpty(value: string | undefined): string | undefined {
100
+ return value?.trim() ? value : undefined;
101
+ }
102
+
103
+ function finalAssistantMessage(previous: AgentPane | undefined, payload: WatcherAgentEventPayload): string | undefined {
104
+ return nonEmpty(payload.finalMessage) ?? nonEmpty(previous?.pendingAssistantMessage) ?? nonEmpty(previous?.lastMessage);
105
+ }
106
+
107
+ export function applyAgentEvent(previous: AgentPane | undefined, input: WatcherAgentEventInput, context: AgentEventContext): AgentPane {
108
+ const now = input.now ?? context.now;
109
+ const status = mapAgentEventToStatus(input.type);
110
+ const pane = basePane(previous, input, context, status);
111
+ const payload = input.payload;
112
+
113
+ if (previous?.status === 'idle') {
114
+ if (input.type === 'assistant-message') {
115
+ const text = payload.text!;
116
+ return {
117
+ ...pane,
118
+ status: 'idle',
119
+ reportedStatus: 'idle',
120
+ summary: previous.summary,
121
+ userMessage: previous.userMessage,
122
+ lastMessage: text,
123
+ pendingAssistantMessage: undefined,
124
+ activityItems: undefined,
125
+ currentAction: undefined,
126
+ };
127
+ }
128
+ if (input.type !== 'session-started' && input.type !== 'agent-started' && input.type !== 'user-message' && input.type !== 'needs-input' && input.type !== 'error') {
129
+ return {
130
+ ...pane,
131
+ status: 'idle',
132
+ reportedStatus: 'idle',
133
+ summary: previous.summary,
134
+ userMessage: previous.userMessage,
135
+ lastMessage: previous.lastMessage,
136
+ pendingAssistantMessage: undefined,
137
+ activityItems: undefined,
138
+ currentAction: undefined,
139
+ };
140
+ }
141
+ }
142
+
143
+ switch (input.type) {
144
+ case 'session-started':
145
+ return {
146
+ ...pane,
147
+ summary: 'Waiting for first task',
148
+ userMessage: undefined,
149
+ lastMessage: undefined,
150
+ pendingAssistantMessage: undefined,
151
+ activityItems: undefined,
152
+ currentAction: undefined,
153
+ };
154
+
155
+ case 'agent-started':
156
+ return {
157
+ ...pane,
158
+ summary: previous?.summary ?? 'Working',
159
+ userMessage: previous?.userMessage,
160
+ lastMessage: undefined,
161
+ pendingAssistantMessage: undefined,
162
+ activityItems: undefined,
163
+ currentAction: 'Working',
164
+ };
165
+
166
+ case 'user-message': {
167
+ const text = payload.text!;
168
+ return {
169
+ ...pane,
170
+ summary: summarize(text, 'User message'),
171
+ userMessage: text,
172
+ lastMessage: undefined,
173
+ pendingAssistantMessage: undefined,
174
+ activityItems: undefined,
175
+ currentAction: undefined,
176
+ };
177
+ }
178
+
179
+ case 'assistant-delta': {
180
+ const text = payload.text!;
181
+ return {
182
+ ...pane,
183
+ summary: summaryAfterNonUserEvent(previous, text, 'Responding'),
184
+ userMessage: previous?.userMessage,
185
+ lastMessage: previous?.lastMessage,
186
+ pendingAssistantMessage: previous?.pendingAssistantMessage,
187
+ currentAction: 'Responding',
188
+ activityItems: mergeActivityItems(previous?.activityItems, [{
189
+ id: assistantActivityId(payload),
190
+ kind: 'assistant',
191
+ label: 'assistant',
192
+ text,
193
+ state: 'running',
194
+ updatedAt: now,
195
+ }]),
196
+ };
197
+ }
198
+
199
+ case 'assistant-message': {
200
+ const text = payload.text!;
201
+ return {
202
+ ...pane,
203
+ summary: summaryAfterNonUserEvent(previous, text, 'Assistant message'),
204
+ userMessage: previous?.userMessage,
205
+ lastMessage: previous?.lastMessage,
206
+ pendingAssistantMessage: text,
207
+ currentAction: undefined,
208
+ activityItems: mergeActivityItems(previous?.activityItems, [{
209
+ id: assistantActivityId(payload),
210
+ kind: 'assistant',
211
+ label: 'assistant',
212
+ text,
213
+ state: 'done',
214
+ updatedAt: now,
215
+ }]),
216
+ };
217
+ }
218
+
219
+ case 'tool-started': {
220
+ const name = payload.name!;
221
+ return {
222
+ ...pane,
223
+ summary: previous?.userMessage ? previous.summary : summarize(`Running ${name}`, `Running ${name}`),
224
+ userMessage: previous?.userMessage,
225
+ lastMessage: previous?.lastMessage,
226
+ pendingAssistantMessage: previous?.pendingAssistantMessage,
227
+ currentAction: name,
228
+ activityItems: mergeActivityItems(previous?.activityItems, [{
229
+ id: toolActivityId(payload)!,
230
+ kind: 'tool',
231
+ label: name,
232
+ text: payload.input,
233
+ state: 'running',
234
+ updatedAt: now,
235
+ }]),
236
+ };
237
+ }
238
+
239
+ case 'tool-updated': {
240
+ const activityId = toolActivityId(payload)!;
241
+ const previousItem = previous?.activityItems?.find((item) => item.id === activityId);
242
+ const name = payload.name ?? previousItem?.label ?? previous?.currentAction ?? 'tool';
243
+ return {
244
+ ...pane,
245
+ summary: previous?.userMessage ? previous.summary : (previous?.summary ?? `Running ${name}`),
246
+ userMessage: previous?.userMessage,
247
+ lastMessage: previous?.lastMessage,
248
+ pendingAssistantMessage: previous?.pendingAssistantMessage,
249
+ currentAction: name,
250
+ activityItems: mergeActivityItems(previous?.activityItems, [{
251
+ id: activityId,
252
+ kind: 'tool',
253
+ label: name,
254
+ text: payload.text ?? previousItem?.text,
255
+ state: 'running',
256
+ updatedAt: now,
257
+ }]),
258
+ };
259
+ }
260
+
261
+ case 'tool-finished': {
262
+ const activityId = toolActivityId(payload)!;
263
+ const previousItem = previous?.activityItems?.find((item) => item.id === activityId);
264
+ const name = payload.name ?? previousItem?.label ?? 'tool';
265
+ return {
266
+ ...pane,
267
+ summary: previous?.userMessage ? previous.summary : (previous?.summary ?? `Finished ${name}`),
268
+ userMessage: previous?.userMessage,
269
+ lastMessage: previous?.lastMessage,
270
+ pendingAssistantMessage: previous?.pendingAssistantMessage,
271
+ currentAction: undefined,
272
+ activityItems: mergeActivityItems(previous?.activityItems, [{
273
+ id: activityId,
274
+ kind: 'tool',
275
+ label: name,
276
+ text: payload.output ?? previousItem?.text,
277
+ state: payload.error ? 'error' : 'done',
278
+ updatedAt: now,
279
+ }]),
280
+ };
281
+ }
282
+
283
+ case 'needs-input': {
284
+ const activityId = toolActivityId(payload);
285
+ const previousItem = activityId ? previous?.activityItems?.find((item) => item.id === activityId) : undefined;
286
+ const updates: AgentActivityItem[] = activityId ? [{
287
+ id: activityId,
288
+ kind: 'tool',
289
+ label: payload.name ?? previousItem?.label ?? 'tool',
290
+ text: payload.text ?? payload.reason ?? previousItem?.text,
291
+ state: 'waiting',
292
+ updatedAt: now,
293
+ }] : [];
294
+ const currentAction = payload.text ?? payload.reason ?? 'Needs input';
295
+ return {
296
+ ...pane,
297
+ summary: previous?.userMessage ? previous.summary : summarize(currentAction, 'Needs input'),
298
+ userMessage: previous?.userMessage,
299
+ lastMessage: previous?.lastMessage,
300
+ pendingAssistantMessage: previous?.pendingAssistantMessage,
301
+ currentAction,
302
+ activityItems: mergeActivityItems(previous?.activityItems, updates),
303
+ };
304
+ }
305
+
306
+ case 'error': {
307
+ const currentAction = payload.reason ?? 'error';
308
+ return {
309
+ ...pane,
310
+ summary: previous?.userMessage ? previous.summary : summarize(currentAction, 'error'),
311
+ userMessage: previous?.userMessage,
312
+ lastMessage: previous?.lastMessage,
313
+ pendingAssistantMessage: previous?.pendingAssistantMessage,
314
+ currentAction,
315
+ activityItems: previous?.activityItems,
316
+ };
317
+ }
318
+
319
+ case 'agent-finished': {
320
+ const finalMessage = finalAssistantMessage(previous, payload);
321
+ return {
322
+ ...pane,
323
+ summary: previous?.userMessage
324
+ ? previous.summary
325
+ : previous?.summary && !isPlaceholderSummary(previous.summary)
326
+ ? previous.summary
327
+ : summarize(finalMessage, 'Finished'),
328
+ userMessage: previous?.userMessage,
329
+ lastMessage: finalMessage,
330
+ pendingAssistantMessage: undefined,
331
+ activityItems: undefined,
332
+ currentAction: undefined,
333
+ };
334
+ }
335
+ }
336
+ }
@@ -0,0 +1,188 @@
1
+ import path from 'node:path';
2
+ import type { AgentType } from './model.js';
3
+ import { isAgentType } from './agents/registry.js';
4
+ import { EventSurfaceIdentity, tmuxSurface } from './surfaceIdentity.js';
5
+
6
+ export const MAX_EVENT_TEXT = 16_000;
7
+
8
+ export const WATCHER_AGENT_EVENT_TYPES = [
9
+ 'session-started',
10
+ 'agent-started',
11
+ 'user-message',
12
+ 'assistant-delta',
13
+ 'assistant-message',
14
+ 'tool-started',
15
+ 'tool-updated',
16
+ 'tool-finished',
17
+ 'needs-input',
18
+ 'agent-finished',
19
+ 'error',
20
+ ] as const;
21
+
22
+ export type WatcherAgentEventType = typeof WATCHER_AGENT_EVENT_TYPES[number];
23
+
24
+ export interface WatcherAgentEventPayload {
25
+ cwd?: string;
26
+ reason?: string;
27
+ text?: string;
28
+ messageId?: string;
29
+ finalMessage?: string;
30
+ id?: string;
31
+ name?: string;
32
+ input?: string;
33
+ output?: string;
34
+ error?: boolean;
35
+ }
36
+
37
+ export interface WatcherAgentEventInput {
38
+ agent: AgentType;
39
+ type: WatcherAgentEventType;
40
+ surface: EventSurfaceIdentity;
41
+ payload: WatcherAgentEventPayload;
42
+ now?: number;
43
+ }
44
+
45
+ export class AgentEventValidationError extends Error {
46
+ constructor(message: string) {
47
+ super(message);
48
+ this.name = 'AgentEventValidationError';
49
+ }
50
+ }
51
+
52
+ export function isWatcherAgentEventType(value: string): value is WatcherAgentEventType {
53
+ return WATCHER_AGENT_EVENT_TYPES.includes(value as WatcherAgentEventType);
54
+ }
55
+
56
+ export function readJsonObject(input: string): Record<string, unknown> {
57
+ if (!input.trim()) return {};
58
+ const parsed = JSON.parse(input) as unknown;
59
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
60
+ throw new AgentEventValidationError('event payload must be a JSON object');
61
+ }
62
+ return parsed as Record<string, unknown>;
63
+ }
64
+
65
+ function requireString(payload: Record<string, unknown>, key: string): string {
66
+ const value = payload[key];
67
+ if (typeof value !== 'string' || !value.trim()) {
68
+ throw new AgentEventValidationError(`event payload requires non-empty string field: ${key}`);
69
+ }
70
+ return capEventText(value.trim());
71
+ }
72
+
73
+ function optionalString(payload: Record<string, unknown>, key: string): string | undefined {
74
+ const value = payload[key];
75
+ if (value === undefined || value === null) return undefined;
76
+ if (typeof value !== 'string') throw new AgentEventValidationError(`event payload field must be a string: ${key}`);
77
+ const text = value.trim();
78
+ return text ? capEventText(text) : undefined;
79
+ }
80
+
81
+ function optionalBoolean(payload: Record<string, unknown>, key: string): boolean | undefined {
82
+ const value = payload[key];
83
+ if (value === undefined || value === null) return undefined;
84
+ if (typeof value !== 'boolean') throw new AgentEventValidationError(`event payload field must be a boolean: ${key}`);
85
+ return value;
86
+ }
87
+
88
+ export function capEventText(value: string): string {
89
+ return value.length > MAX_EVENT_TEXT ? `${value.slice(0, MAX_EVENT_TEXT - 1)}…` : value;
90
+ }
91
+
92
+ export function compactEventValue(value: unknown): string | undefined {
93
+ if (value === undefined || value === null) return undefined;
94
+ let text: string;
95
+ try {
96
+ text = typeof value === 'string' ? value : JSON.stringify(value);
97
+ } catch {
98
+ text = String(value);
99
+ }
100
+ text = (text ?? '').replace(/\s+/g, ' ').trim();
101
+ return text ? capEventText(text) : undefined;
102
+ }
103
+
104
+ function optionalCompactValue(payload: Record<string, unknown>, key: string): string | undefined {
105
+ if (!(key in payload)) return undefined;
106
+ return compactEventValue(payload[key]);
107
+ }
108
+
109
+ function optionalCwd(payload: Record<string, unknown>): string | undefined {
110
+ const cwd = optionalString(payload, 'cwd');
111
+ return cwd && path.isAbsolute(cwd) ? cwd : undefined;
112
+ }
113
+
114
+ function parseSurface(value: unknown): EventSurfaceIdentity | undefined {
115
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
116
+ const record = value as Record<string, unknown>;
117
+ if (record.backend !== 'tmux') throw new AgentEventValidationError('event surface backend must be tmux');
118
+ if (typeof record.id !== 'string' || !record.id.trim()) throw new AgentEventValidationError('event surface requires non-empty id');
119
+ return { backend: 'tmux', id: record.id.trim() };
120
+ }
121
+
122
+ export function resolveEventSurface(payload: Record<string, unknown>, fallbackTmuxPaneId?: string): EventSurfaceIdentity {
123
+ const explicit = parseSurface(payload.surface);
124
+ if (explicit) return explicit;
125
+ if (fallbackTmuxPaneId?.trim()) return tmuxSurface(fallbackTmuxPaneId.trim());
126
+ throw new AgentEventValidationError('event requires surface identity or TMUX_PANE fallback');
127
+ }
128
+
129
+ export function normalizeWatcherAgentEventPayload(type: WatcherAgentEventType, payload: Record<string, unknown>): WatcherAgentEventPayload {
130
+ const common: WatcherAgentEventPayload = { cwd: optionalCwd(payload) };
131
+ switch (type) {
132
+ case 'session-started':
133
+ return { ...common, reason: optionalString(payload, 'reason') };
134
+ case 'agent-started':
135
+ return common;
136
+ case 'user-message':
137
+ return { ...common, text: requireString(payload, 'text') };
138
+ case 'assistant-delta':
139
+ case 'assistant-message':
140
+ return { ...common, messageId: optionalString(payload, 'messageId'), text: requireString(payload, 'text') };
141
+ case 'tool-started':
142
+ return { ...common, id: requireString(payload, 'id'), name: requireString(payload, 'name'), input: optionalCompactValue(payload, 'input') };
143
+ case 'tool-updated':
144
+ return { ...common, id: requireString(payload, 'id'), name: optionalString(payload, 'name'), text: optionalString(payload, 'text') };
145
+ case 'tool-finished':
146
+ return {
147
+ ...common,
148
+ id: requireString(payload, 'id'),
149
+ name: optionalString(payload, 'name'),
150
+ output: optionalCompactValue(payload, 'output'),
151
+ error: optionalBoolean(payload, 'error'),
152
+ };
153
+ case 'needs-input':
154
+ return {
155
+ ...common,
156
+ reason: optionalString(payload, 'reason'),
157
+ text: optionalString(payload, 'text'),
158
+ id: optionalString(payload, 'id'),
159
+ name: optionalString(payload, 'name'),
160
+ };
161
+ case 'agent-finished':
162
+ return { ...common, finalMessage: optionalString(payload, 'finalMessage') };
163
+ case 'error':
164
+ return { ...common, reason: optionalString(payload, 'reason') };
165
+ }
166
+ }
167
+
168
+ export interface BuildWatcherAgentEventOptions {
169
+ fallbackTmuxPaneId?: string;
170
+ now?: number;
171
+ }
172
+
173
+ export function buildWatcherAgentEventInput(
174
+ agent: string | undefined,
175
+ type: string | undefined,
176
+ payload: Record<string, unknown>,
177
+ options: BuildWatcherAgentEventOptions = {},
178
+ ): WatcherAgentEventInput {
179
+ if (!agent || !isAgentType(agent)) throw new AgentEventValidationError(`unknown agent integration: ${agent ?? '(missing)'}`);
180
+ if (!type || !isWatcherAgentEventType(type)) throw new AgentEventValidationError(`unknown Watcher Agent Event: ${type ?? '(missing)'}`);
181
+ return {
182
+ agent,
183
+ type,
184
+ surface: resolveEventSurface(payload, options.fallbackTmuxPaneId),
185
+ payload: normalizeWatcherAgentEventPayload(type, payload),
186
+ now: options.now,
187
+ };
188
+ }
@@ -0,0 +1,16 @@
1
+ import { AgentIntegration, commandAliasDetector } from './types.js';
2
+
3
+ const commandAliases = ['claude', 'claude-code'];
4
+
5
+ export const claudeIntegration: AgentIntegration = {
6
+ type: 'claude',
7
+ displayName: 'Claude',
8
+ commandAliases,
9
+ capabilities: {
10
+ eventIngestion: 'supported',
11
+ eventSourceInstall: 'not-implemented',
12
+ activityEvents: 'unknown',
13
+ assistantDeltas: 'unknown',
14
+ },
15
+ detectProcess: commandAliasDetector(commandAliases),
16
+ };
@@ -0,0 +1,16 @@
1
+ import { AgentIntegration, commandAliasDetector } from './types.js';
2
+
3
+ const commandAliases = ['codex'];
4
+
5
+ export const codexIntegration: AgentIntegration = {
6
+ type: 'codex',
7
+ displayName: 'Codex',
8
+ commandAliases,
9
+ capabilities: {
10
+ eventIngestion: 'supported',
11
+ eventSourceInstall: 'not-implemented',
12
+ activityEvents: 'not-implemented',
13
+ assistantDeltas: 'unknown',
14
+ },
15
+ detectProcess: commandAliasDetector(commandAliases),
16
+ };
@@ -0,0 +1,16 @@
1
+ import { AgentIntegration, commandAliasDetector } from './types.js';
2
+
3
+ const commandAliases = ['opencode'];
4
+
5
+ export const opencodeIntegration: AgentIntegration = {
6
+ type: 'opencode',
7
+ displayName: 'OpenCode',
8
+ commandAliases,
9
+ capabilities: {
10
+ eventIngestion: 'supported',
11
+ eventSourceInstall: 'not-implemented',
12
+ activityEvents: 'not-implemented',
13
+ assistantDeltas: 'not-implemented',
14
+ },
15
+ detectProcess: commandAliasDetector(commandAliases),
16
+ };
@@ -0,0 +1,16 @@
1
+ import { AgentIntegration, commandAliasDetector } from './types.js';
2
+
3
+ const commandAliases = ['pi'];
4
+
5
+ export const piIntegration: AgentIntegration = {
6
+ type: 'pi',
7
+ displayName: 'Pi',
8
+ commandAliases,
9
+ capabilities: {
10
+ eventIngestion: 'supported',
11
+ eventSourceInstall: 'supported',
12
+ activityEvents: 'supported',
13
+ assistantDeltas: 'not-implemented',
14
+ },
15
+ detectProcess: commandAliasDetector(commandAliases),
16
+ };
@@ -0,0 +1,54 @@
1
+ import path from 'node:path';
2
+ import type { AgentType } from '../model.js';
3
+ import type { AgentIntegration, AgentProcessInfo } from './types.js';
4
+ import { claudeIntegration } from './claude.js';
5
+ import { codexIntegration } from './codex.js';
6
+ import { opencodeIntegration } from './opencode.js';
7
+ import { piIntegration } from './pi.js';
8
+
9
+ export const AGENT_INTEGRATIONS = [
10
+ piIntegration,
11
+ claudeIntegration,
12
+ codexIntegration,
13
+ opencodeIntegration,
14
+ ] as const satisfies readonly AgentIntegration[];
15
+
16
+ export const AGENT_TYPES = AGENT_INTEGRATIONS.map((integration) => integration.type) as AgentType[];
17
+
18
+ export function isAgentType(value: string): value is AgentType {
19
+ return AGENT_TYPES.includes(value as AgentType);
20
+ }
21
+
22
+ export function getAgentIntegration(agent: AgentType): AgentIntegration {
23
+ const integration = AGENT_INTEGRATIONS.find((candidate) => candidate.type === agent);
24
+ if (!integration) throw new Error(`unknown agent integration: ${agent}`);
25
+ return integration;
26
+ }
27
+
28
+ export function normalizeAgentCommand(command: string | undefined): string | undefined {
29
+ if (!command) return undefined;
30
+ const base = path.basename(command).toLowerCase();
31
+ return base.replace(/\.(js|mjs|cjs|ts|tsx)$/u, '');
32
+ }
33
+
34
+ export function detectAgentFromProcess(process: AgentProcessInfo): AgentType | undefined {
35
+ const normalized: AgentProcessInfo = {
36
+ ...process,
37
+ command: normalizeAgentCommand(process.command),
38
+ };
39
+ return AGENT_INTEGRATIONS.find((integration) => integration.detectProcess(normalized))?.type;
40
+ }
41
+
42
+ export function commandAliasCollisions(): string[] {
43
+ const seen = new Map<string, AgentType>();
44
+ const collisions: string[] = [];
45
+ for (const integration of AGENT_INTEGRATIONS) {
46
+ for (const alias of integration.commandAliases) {
47
+ const normalized = normalizeAgentCommand(alias) ?? alias;
48
+ const previous = seen.get(normalized);
49
+ if (previous && previous !== integration.type) collisions.push(`${normalized}:${previous}/${integration.type}`);
50
+ seen.set(normalized, integration.type);
51
+ }
52
+ }
53
+ return collisions;
54
+ }
@@ -0,0 +1,28 @@
1
+ import type { AgentType } from '../model.js';
2
+
3
+ export type CapabilitySupport = 'supported' | 'not-implemented' | 'unsupported' | 'unknown';
4
+
5
+ export interface AgentIntegrationCapabilities {
6
+ eventIngestion: CapabilitySupport;
7
+ eventSourceInstall: CapabilitySupport;
8
+ activityEvents: CapabilitySupport;
9
+ assistantDeltas: CapabilitySupport;
10
+ }
11
+
12
+ export interface AgentProcessInfo {
13
+ command?: string;
14
+ args?: string;
15
+ }
16
+
17
+ export interface AgentIntegration {
18
+ type: AgentType;
19
+ displayName: string;
20
+ commandAliases: string[];
21
+ capabilities: AgentIntegrationCapabilities;
22
+ detectProcess(process: AgentProcessInfo): boolean;
23
+ }
24
+
25
+ export function commandAliasDetector(commandAliases: string[]): (process: AgentProcessInfo) => boolean {
26
+ const aliases = new Set(commandAliases);
27
+ return (process) => Boolean(process.command && aliases.has(process.command));
28
+ }