agent-vision-mcp 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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +117 -0
  3. package/dist/browser/cdp/browser-cdp-discovery-service.d.ts +10 -0
  4. package/dist/browser/cdp/browser-cdp-discovery-service.js +28 -0
  5. package/dist/browser/cdp/browser-live-tab-service.d.ts +16 -0
  6. package/dist/browser/cdp/browser-live-tab-service.js +42 -0
  7. package/dist/browser/cdp/browser-see-service.d.ts +33 -0
  8. package/dist/browser/cdp/browser-see-service.js +76 -0
  9. package/dist/browser/cdp/browser-tab-context-service.d.ts +23 -0
  10. package/dist/browser/cdp/browser-tab-context-service.js +90 -0
  11. package/dist/browser/cdp/browser-tab-resolution-service.d.ts +9 -0
  12. package/dist/browser/cdp/browser-tab-resolution-service.js +65 -0
  13. package/dist/browser/cdp/browser-tab-screenshot-service.d.ts +20 -0
  14. package/dist/browser/cdp/browser-tab-screenshot-service.js +59 -0
  15. package/dist/browser/cdp/cdp-websocket-session.d.ts +9 -0
  16. package/dist/browser/cdp/cdp-websocket-session.js +99 -0
  17. package/dist/browser/cdp/chrome-cdp-client.d.ts +12 -0
  18. package/dist/browser/cdp/chrome-cdp-client.js +141 -0
  19. package/dist/browser/cdp/live-browser-tab-registry.d.ts +12 -0
  20. package/dist/browser/cdp/live-browser-tab-registry.js +96 -0
  21. package/dist/browser/cdp/png-metadata.d.ts +5 -0
  22. package/dist/browser/cdp/png-metadata.js +16 -0
  23. package/dist/browser/cdp/tab-model.d.ts +33 -0
  24. package/dist/browser/cdp/tab-model.js +15 -0
  25. package/dist/browser/cdp/tab-resolution.d.ts +27 -0
  26. package/dist/browser/cdp/tab-resolution.js +48 -0
  27. package/dist/browser/cdp/types.d.ts +71 -0
  28. package/dist/browser/cdp/types.js +1 -0
  29. package/dist/capture/capture-pipeline.d.ts +5 -0
  30. package/dist/capture/capture-pipeline.js +1 -0
  31. package/dist/capture/create-screen-capture-provider.d.ts +3 -0
  32. package/dist/capture/create-screen-capture-provider.js +8 -0
  33. package/dist/capture/in-memory-capture-pipeline.d.ts +13 -0
  34. package/dist/capture/in-memory-capture-pipeline.js +52 -0
  35. package/dist/capture/in-memory-image-compositor.d.ts +5 -0
  36. package/dist/capture/in-memory-image-compositor.js +34 -0
  37. package/dist/capture/linux-portal-screenshot-provider.d.ts +8 -0
  38. package/dist/capture/linux-portal-screenshot-provider.js +181 -0
  39. package/dist/capture/mock-screen-capture-provider.d.ts +5 -0
  40. package/dist/capture/mock-screen-capture-provider.js +22 -0
  41. package/dist/capture/png-metadata.d.ts +5 -0
  42. package/dist/capture/png-metadata.js +18 -0
  43. package/dist/capture/screen-capture-provider.d.ts +4 -0
  44. package/dist/capture/screen-capture-provider.js +1 -0
  45. package/dist/capture/types.d.ts +38 -0
  46. package/dist/capture/types.js +1 -0
  47. package/dist/cdp-demo.d.ts +1 -0
  48. package/dist/cdp-demo.js +41 -0
  49. package/dist/demo.d.ts +1 -0
  50. package/dist/demo.js +54 -0
  51. package/dist/desktop/capture-now.d.ts +1 -0
  52. package/dist/desktop/capture-now.js +48 -0
  53. package/dist/desktop/controller.d.ts +25 -0
  54. package/dist/desktop/controller.js +77 -0
  55. package/dist/desktop/main.d.ts +1 -0
  56. package/dist/desktop/main.js +80 -0
  57. package/dist/desktop/preload.d.ts +1 -0
  58. package/dist/desktop/preload.js +26 -0
  59. package/dist/desktop/types.d.ts +31 -0
  60. package/dist/desktop/types.js +1 -0
  61. package/dist/errors/app-error.d.ts +7 -0
  62. package/dist/errors/app-error.js +11 -0
  63. package/dist/flow/types.d.ts +48 -0
  64. package/dist/flow/types.js +1 -0
  65. package/dist/flow/visual-capture-flow.d.ts +13 -0
  66. package/dist/flow/visual-capture-flow.js +196 -0
  67. package/dist/index.d.ts +1 -0
  68. package/dist/index.js +3 -0
  69. package/dist/logging/logger.d.ts +15 -0
  70. package/dist/logging/logger.js +28 -0
  71. package/dist/mcp/stdio-server.d.ts +19 -0
  72. package/dist/mcp/stdio-server.js +272 -0
  73. package/dist/mcp/tool-registry.d.ts +21 -0
  74. package/dist/mcp/tool-registry.js +33 -0
  75. package/dist/mcp-stdio.d.ts +2 -0
  76. package/dist/mcp-stdio.js +8 -0
  77. package/dist/overlay/local-overlay-agent.d.ts +46 -0
  78. package/dist/overlay/local-overlay-agent.js +551 -0
  79. package/dist/overlay/overlay-bundle-factory.d.ts +4 -0
  80. package/dist/overlay/overlay-bundle-factory.js +24 -0
  81. package/dist/overlay/types.d.ts +83 -0
  82. package/dist/overlay/types.js +1 -0
  83. package/dist/server.d.ts +19 -0
  84. package/dist/server.js +158 -0
  85. package/dist/session/capture-session-service.d.ts +21 -0
  86. package/dist/session/capture-session-service.js +50 -0
  87. package/dist/session/session-manager.d.ts +29 -0
  88. package/dist/session/session-manager.js +217 -0
  89. package/dist/session/session-store.d.ts +8 -0
  90. package/dist/session/session-store.js +15 -0
  91. package/dist/session/session-waiter.d.ts +14 -0
  92. package/dist/session/session-waiter.js +102 -0
  93. package/dist/types/annotation.d.ts +32 -0
  94. package/dist/types/annotation.js +1 -0
  95. package/dist/types/capture.d.ts +33 -0
  96. package/dist/types/capture.js +1 -0
  97. package/dist/types/session.d.ts +36 -0
  98. package/dist/types/session.js +1 -0
  99. package/package.json +38 -0
package/dist/server.js ADDED
@@ -0,0 +1,158 @@
1
+ import { BrowserCdpDiscoveryService } from "./browser/cdp/browser-cdp-discovery-service.js";
2
+ import { BrowserLiveTabService } from "./browser/cdp/browser-live-tab-service.js";
3
+ import { BrowserSeeService } from "./browser/cdp/browser-see-service.js";
4
+ import { BrowserTabContextService } from "./browser/cdp/browser-tab-context-service.js";
5
+ import { BrowserTabResolutionService } from "./browser/cdp/browser-tab-resolution-service.js";
6
+ import { BrowserTabScreenshotService } from "./browser/cdp/browser-tab-screenshot-service.js";
7
+ import { ChromeCdpClient } from "./browser/cdp/chrome-cdp-client.js";
8
+ import { LiveBrowserTabRegistry } from "./browser/cdp/live-browser-tab-registry.js";
9
+ import { AppError } from "./errors/app-error.js";
10
+ import { ConsoleLogger } from "./logging/logger.js";
11
+ import { ToolRegistry } from "./mcp/tool-registry.js";
12
+ const STRING_SCHEMA = (description) => ({
13
+ type: "string",
14
+ description
15
+ });
16
+ const NUMBER_SCHEMA = (description) => ({
17
+ type: "number",
18
+ description
19
+ });
20
+ const OPTIONAL_QUERY_SCHEMA = {
21
+ type: "object",
22
+ properties: {
23
+ query: STRING_SCHEMA("Optional tab title or URL fragment to match against live browser tabs.")
24
+ },
25
+ additionalProperties: false
26
+ };
27
+ const OPTIONAL_ENDPOINT_SCHEMA = {
28
+ type: "object",
29
+ properties: {
30
+ endpoint: STRING_SCHEMA("Optional Chrome DevTools Protocol base URL, for example http://127.0.0.1:9222.")
31
+ },
32
+ additionalProperties: false
33
+ };
34
+ const OPTIONAL_PRUNE_SCHEMA = {
35
+ type: "object",
36
+ properties: {
37
+ maxAgeMs: NUMBER_SCHEMA("Optional stale-tab age threshold in milliseconds.")
38
+ },
39
+ additionalProperties: false
40
+ };
41
+ const EMPTY_OBJECT_SCHEMA = {
42
+ type: "object",
43
+ properties: {},
44
+ additionalProperties: false
45
+ };
46
+ const READ_ONLY_ANNOTATIONS = {
47
+ readOnlyHint: true
48
+ };
49
+ const readOptionalString = (value, field) => {
50
+ if (value === undefined) {
51
+ return undefined;
52
+ }
53
+ if (typeof value !== "string") {
54
+ throw new AppError("INVALID_ARGUMENT", `${field} must be a string`);
55
+ }
56
+ return value;
57
+ };
58
+ const readOptionalEndpoint = (args) => readOptionalString(args.endpoint, "endpoint");
59
+ const readOptionalNumber = (value, field) => {
60
+ if (value === undefined) {
61
+ return undefined;
62
+ }
63
+ if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
64
+ throw new AppError("INVALID_ARGUMENT", `${field} must be a non-negative number`);
65
+ }
66
+ return value;
67
+ };
68
+ export class VisualContextServer {
69
+ logger = new ConsoleLogger("visual-context-server");
70
+ cdpClient = new ChromeCdpClient(this.logger);
71
+ cdpDiscovery = new BrowserCdpDiscoveryService(this.cdpClient, this.logger);
72
+ liveTabs = new LiveBrowserTabRegistry(this.logger);
73
+ browserLiveTabs = new BrowserLiveTabService(this.cdpDiscovery, this.liveTabs, this.logger);
74
+ tabResolution = new BrowserTabResolutionService(this.browserLiveTabs, this.logger);
75
+ tabScreenshots = new BrowserTabScreenshotService(this.tabResolution, this.logger);
76
+ tabContext = new BrowserTabContextService(this.tabResolution, this.logger);
77
+ browserSee = new BrowserSeeService(this.browserLiveTabs, this.tabScreenshots, this.tabContext, this.logger);
78
+ tools = new ToolRegistry(this.logger);
79
+ constructor() {
80
+ this.registerTools();
81
+ }
82
+ listTools() {
83
+ return this.tools.list();
84
+ }
85
+ async callTool(name, args = {}) {
86
+ return this.tools.call(name, args);
87
+ }
88
+ start() {
89
+ this.logger.info("CDP-first visual context server ready", {
90
+ tools: this.listTools().map((tool) => tool.name)
91
+ });
92
+ }
93
+ registerTools() {
94
+ this.tools.register({
95
+ name: "getBrowserCdpStatus",
96
+ description: "Use this when you need to verify that the MCP can reach a Chrome DevTools Protocol endpoint.",
97
+ inputSchema: OPTIONAL_ENDPOINT_SCHEMA,
98
+ annotations: READ_ONLY_ANNOTATIONS,
99
+ handler: (args) => this.cdpDiscovery.getConnectionStatus(readOptionalEndpoint(args))
100
+ });
101
+ this.tools.register({
102
+ name: "discoverBrowserTabsViaCdp",
103
+ description: "Use this when you need the raw list of browser tabs currently exposed by Chrome DevTools Protocol.",
104
+ inputSchema: OPTIONAL_ENDPOINT_SCHEMA,
105
+ annotations: READ_ONLY_ANNOTATIONS,
106
+ handler: (args) => this.cdpDiscovery.discoverTabs(readOptionalEndpoint(args))
107
+ });
108
+ this.tools.register({
109
+ name: "refreshLiveBrowserTabs",
110
+ description: "Use this when you want to refresh the normalized live browser-tab cache from Chrome DevTools Protocol.",
111
+ inputSchema: OPTIONAL_ENDPOINT_SCHEMA,
112
+ annotations: READ_ONLY_ANNOTATIONS,
113
+ handler: (args) => this.browserLiveTabs.refresh(readOptionalEndpoint(args))
114
+ });
115
+ this.tools.register({
116
+ name: "listLiveBrowserTabs",
117
+ description: "Use this when you want the current cached live browser-tab model without re-querying Chrome.",
118
+ inputSchema: EMPTY_OBJECT_SCHEMA,
119
+ annotations: READ_ONLY_ANNOTATIONS,
120
+ handler: () => this.browserLiveTabs.list()
121
+ });
122
+ this.tools.register({
123
+ name: "pruneStaleLiveBrowserTabs",
124
+ description: "Use this when you want to remove stale cached browser tabs that have not been refreshed recently.",
125
+ inputSchema: OPTIONAL_PRUNE_SCHEMA,
126
+ annotations: READ_ONLY_ANNOTATIONS,
127
+ handler: (args) => this.browserLiveTabs.pruneStale(readOptionalNumber(args.maxAgeMs, "maxAgeMs"))
128
+ });
129
+ this.tools.register({
130
+ name: "resolveLiveBrowserTab",
131
+ description: "Use this when you want to resolve the active or best matching browser tab for a /see-style query.",
132
+ inputSchema: OPTIONAL_QUERY_SCHEMA,
133
+ annotations: READ_ONLY_ANNOTATIONS,
134
+ handler: (args) => this.tabResolution.resolve(readOptionalString(args.query, "query"))
135
+ });
136
+ this.tools.register({
137
+ name: "captureResolvedBrowserTabScreenshot",
138
+ description: "Use this when you need a real PNG screenshot from the resolved live browser tab through CDP.",
139
+ inputSchema: OPTIONAL_QUERY_SCHEMA,
140
+ annotations: READ_ONLY_ANNOTATIONS,
141
+ handler: (args) => this.tabScreenshots.captureResolved(readOptionalString(args.query, "query"))
142
+ });
143
+ this.tools.register({
144
+ name: "getResolvedBrowserTabContext",
145
+ description: "Use this when you need structured page metadata and visible text from the resolved live browser tab.",
146
+ inputSchema: OPTIONAL_QUERY_SCHEMA,
147
+ annotations: READ_ONLY_ANNOTATIONS,
148
+ handler: (args) => this.tabContext.getResolvedContext(readOptionalString(args.query, "query"))
149
+ });
150
+ this.tools.register({
151
+ name: "seeBrowserTabViaCdp",
152
+ description: "Use this for the high-level browser-first /see flow: resolve a live tab, capture it, and return structured page context.",
153
+ inputSchema: OPTIONAL_QUERY_SCHEMA,
154
+ annotations: READ_ONLY_ANNOTATIONS,
155
+ handler: (args) => this.browserSee.see(readOptionalString(args.query, "query"))
156
+ });
157
+ }
158
+ }
@@ -0,0 +1,21 @@
1
+ import type { Logger } from "../logging/logger.js";
2
+ import type { AwaitCaptureSessionInput, AwaitCaptureSessionResult, CaptureSession, CompleteCaptureSessionInput, StartCaptureSessionInput } from "../types/session.js";
3
+ import { SessionManager } from "./session-manager.js";
4
+ import { SessionWaiter } from "./session-waiter.js";
5
+ export declare class CaptureSessionService {
6
+ private readonly sessionManager;
7
+ private readonly sessionWaiter;
8
+ private readonly logger;
9
+ constructor(sessionManager: SessionManager, sessionWaiter: SessionWaiter, logger: Logger);
10
+ startSession(input: StartCaptureSessionInput): CaptureSession;
11
+ awaitSession(input: AwaitCaptureSessionInput): Promise<AwaitCaptureSessionResult>;
12
+ getSession(sessionId: string): CaptureSession;
13
+ listSessions(): CaptureSession[];
14
+ completeSession(input: CompleteCaptureSessionInput): CaptureSession;
15
+ cancelSession(sessionId: string): CaptureSession;
16
+ failSession(sessionId: string, errorMessage: string): CaptureSession;
17
+ reapTerminalSessions(maxAgeMs: number): {
18
+ removedSessionIds: string[];
19
+ };
20
+ logStateSummary(): void;
21
+ }
@@ -0,0 +1,50 @@
1
+ export class CaptureSessionService {
2
+ sessionManager;
3
+ sessionWaiter;
4
+ logger;
5
+ constructor(sessionManager, sessionWaiter, logger) {
6
+ this.sessionManager = sessionManager;
7
+ this.sessionWaiter = sessionWaiter;
8
+ this.logger = logger;
9
+ }
10
+ startSession(input) {
11
+ const session = this.sessionManager.startSession(input);
12
+ return this.sessionManager.activateSession(session.id);
13
+ }
14
+ awaitSession(input) {
15
+ return this.sessionWaiter.awaitSession(input);
16
+ }
17
+ getSession(sessionId) {
18
+ return this.sessionManager.getSession(sessionId);
19
+ }
20
+ listSessions() {
21
+ return this.sessionManager.listSessions();
22
+ }
23
+ completeSession(input) {
24
+ const completed = this.sessionManager.completeSession(input);
25
+ this.sessionWaiter.notify(completed);
26
+ return completed;
27
+ }
28
+ cancelSession(sessionId) {
29
+ const cancelled = this.sessionManager.cancelSession(sessionId);
30
+ this.sessionWaiter.notify(cancelled);
31
+ return cancelled;
32
+ }
33
+ failSession(sessionId, errorMessage) {
34
+ const failed = this.sessionManager.failSession(sessionId, errorMessage);
35
+ this.sessionWaiter.notify(failed);
36
+ return failed;
37
+ }
38
+ reapTerminalSessions(maxAgeMs) {
39
+ const result = this.sessionManager.reapTerminalSessions(maxAgeMs);
40
+ for (const sessionId of result.removedSessionIds) {
41
+ this.sessionWaiter.clearSession(sessionId);
42
+ }
43
+ return result;
44
+ }
45
+ logStateSummary() {
46
+ this.logger.debug("Capture session service state", {
47
+ sessions: this.listSessions().length
48
+ });
49
+ }
50
+ }
@@ -0,0 +1,29 @@
1
+ import type { Logger } from "../logging/logger.js";
2
+ import type { CaptureBundle } from "../types/capture.js";
3
+ import type { CaptureSession, CaptureSessionStatus, CompleteCaptureSessionInput, StartCaptureSessionInput } from "../types/session.js";
4
+ import { SessionStore } from "./session-store.js";
5
+ export declare class SessionManager {
6
+ private readonly store;
7
+ private readonly logger;
8
+ constructor(store: SessionStore, logger: Logger);
9
+ startSession(input: StartCaptureSessionInput): CaptureSession;
10
+ activateSession(sessionId: string): CaptureSession;
11
+ completeSession(input: CompleteCaptureSessionInput): CaptureSession;
12
+ cancelSession(sessionId: string): CaptureSession;
13
+ failSession(sessionId: string, errorMessage: string): CaptureSession;
14
+ getSession(sessionId: string): CaptureSession;
15
+ listSessions(): CaptureSession[];
16
+ isTerminalStatus(status: CaptureSessionStatus): boolean;
17
+ reapTerminalSessions(maxAgeMs: number): {
18
+ removedSessionIds: string[];
19
+ };
20
+ private requireNonTerminalSession;
21
+ private assertFresh;
22
+ private assertNotExpired;
23
+ private isExpired;
24
+ private expireSession;
25
+ private persistWithStatus;
26
+ private persist;
27
+ private requireSession;
28
+ }
29
+ export declare const createMockCaptureBundle: (sessionId: string, command: CaptureBundle["command"]) => CaptureBundle;
@@ -0,0 +1,217 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { AppError } from "../errors/app-error.js";
3
+ const DEFAULT_SESSION_TTL_MS = 5 * 60 * 1000;
4
+ const TERMINAL_STATUSES = new Set([
5
+ "completed",
6
+ "cancelled",
7
+ "expired",
8
+ "failed"
9
+ ]);
10
+ export class SessionManager {
11
+ store;
12
+ logger;
13
+ constructor(store, logger) {
14
+ this.store = store;
15
+ this.logger = logger;
16
+ }
17
+ startSession(input) {
18
+ const now = new Date();
19
+ const ttlMs = input.ttlMs ?? DEFAULT_SESSION_TTL_MS;
20
+ const session = {
21
+ id: randomUUID(),
22
+ command: input.command,
23
+ status: "created",
24
+ createdAt: now.toISOString(),
25
+ updatedAt: now.toISOString(),
26
+ expiresAt: new Date(now.getTime() + ttlMs).toISOString()
27
+ };
28
+ this.store.set(session);
29
+ this.logger.info("Started capture session", {
30
+ sessionId: session.id,
31
+ command: session.command,
32
+ expiresAt: session.expiresAt
33
+ });
34
+ return session;
35
+ }
36
+ activateSession(sessionId) {
37
+ const session = this.requireNonTerminalSession(sessionId);
38
+ return this.persistWithStatus(session, "active", "Activated capture session");
39
+ }
40
+ completeSession(input) {
41
+ const session = this.requireNonTerminalSession(input.sessionId);
42
+ const updated = this.persist({
43
+ ...session,
44
+ status: "completed",
45
+ result: input.bundle,
46
+ errorMessage: undefined
47
+ });
48
+ this.logger.info("Completed capture session", {
49
+ sessionId: updated.id
50
+ });
51
+ return updated;
52
+ }
53
+ cancelSession(sessionId) {
54
+ const session = this.requireSession(sessionId);
55
+ if (session.status === "completed") {
56
+ throw new AppError("SESSION_CONFLICT", "Completed sessions cannot be cancelled", {
57
+ sessionId: session.id
58
+ });
59
+ }
60
+ if (this.isTerminalStatus(session.status)) {
61
+ return session;
62
+ }
63
+ return this.persistWithStatus(session, "cancelled", "Cancelled capture session");
64
+ }
65
+ failSession(sessionId, errorMessage) {
66
+ const session = this.requireSession(sessionId);
67
+ if (this.isTerminalStatus(session.status)) {
68
+ return session;
69
+ }
70
+ const updated = this.persist({
71
+ ...session,
72
+ status: "failed",
73
+ errorMessage
74
+ });
75
+ this.logger.error("Failed capture session", {
76
+ sessionId: updated.id,
77
+ errorMessage
78
+ });
79
+ return updated;
80
+ }
81
+ getSession(sessionId) {
82
+ const session = this.requireSession(sessionId);
83
+ return this.assertFresh(session);
84
+ }
85
+ listSessions() {
86
+ return this.store.list().map((session) => this.assertFresh(session));
87
+ }
88
+ isTerminalStatus(status) {
89
+ return TERMINAL_STATUSES.has(status);
90
+ }
91
+ reapTerminalSessions(maxAgeMs) {
92
+ const now = Date.now();
93
+ const removedSessionIds = [];
94
+ for (const session of this.listSessions()) {
95
+ if (!this.isTerminalStatus(session.status)) {
96
+ continue;
97
+ }
98
+ const updatedAtMs = Date.parse(session.updatedAt);
99
+ if (now - updatedAtMs < maxAgeMs) {
100
+ continue;
101
+ }
102
+ this.store.delete(session.id);
103
+ removedSessionIds.push(session.id);
104
+ }
105
+ if (removedSessionIds.length > 0) {
106
+ this.logger.info("Reaped terminal capture sessions", {
107
+ removedSessionIds,
108
+ maxAgeMs
109
+ });
110
+ }
111
+ return { removedSessionIds };
112
+ }
113
+ requireNonTerminalSession(sessionId) {
114
+ const session = this.requireSession(sessionId);
115
+ this.assertNotExpired(session);
116
+ if (this.isTerminalStatus(session.status)) {
117
+ throw new AppError("SESSION_CONFLICT", "Session cannot transition from its current state", {
118
+ sessionId: session.id,
119
+ status: session.status
120
+ });
121
+ }
122
+ return session;
123
+ }
124
+ assertFresh(session) {
125
+ return this.isExpired(session) ? this.expireSession(session) : session;
126
+ }
127
+ assertNotExpired(session) {
128
+ if (this.isExpired(session)) {
129
+ this.expireSession(session);
130
+ throw new AppError("SESSION_EXPIRED", "Session has expired", {
131
+ sessionId: session.id
132
+ });
133
+ }
134
+ }
135
+ isExpired(session) {
136
+ return Date.now() > Date.parse(session.expiresAt);
137
+ }
138
+ expireSession(session) {
139
+ if (session.status === "expired") {
140
+ return session;
141
+ }
142
+ const updated = this.persist({
143
+ ...session,
144
+ status: "expired"
145
+ });
146
+ this.logger.warn("Expired capture session", {
147
+ sessionId: updated.id
148
+ });
149
+ return updated;
150
+ }
151
+ persistWithStatus(session, status, logMessage) {
152
+ const updated = this.persist({
153
+ ...session,
154
+ status
155
+ });
156
+ this.logger.debug(logMessage, {
157
+ sessionId: updated.id,
158
+ status: updated.status
159
+ });
160
+ return updated;
161
+ }
162
+ persist(session) {
163
+ const now = new Date().toISOString();
164
+ const updated = {
165
+ ...session,
166
+ updatedAt: now
167
+ };
168
+ this.store.set(updated);
169
+ return updated;
170
+ }
171
+ requireSession(sessionId) {
172
+ const session = this.store.get(sessionId);
173
+ if (!session) {
174
+ throw new AppError("NOT_FOUND", "Capture session not found", {
175
+ sessionId
176
+ });
177
+ }
178
+ return session;
179
+ }
180
+ }
181
+ export const createMockCaptureBundle = (sessionId, command) => ({
182
+ sessionId,
183
+ command,
184
+ image: {
185
+ mimeType: "image/png",
186
+ bytesBase64: "cGg3LW1vY2staW1hZ2U=",
187
+ width: 1280,
188
+ height: 720,
189
+ byteLength: 14,
190
+ sourceWidth: 1920,
191
+ sourceHeight: 1080,
192
+ backend: "mock-capture-bundle",
193
+ persisted: false
194
+ },
195
+ selection: {
196
+ x: 120,
197
+ y: 96,
198
+ width: 640,
199
+ height: 360
200
+ },
201
+ annotations: [
202
+ {
203
+ type: "rect",
204
+ x: 150,
205
+ y: 120,
206
+ width: 280,
207
+ height: 96,
208
+ label: "Mock highlighted region"
209
+ }
210
+ ],
211
+ context: {
212
+ activeAppName: "Mock Browser",
213
+ activeWindowTitle: "Phase 7 Prototype",
214
+ capturedAt: new Date().toISOString(),
215
+ displayId: "display-1"
216
+ }
217
+ });
@@ -0,0 +1,8 @@
1
+ import type { CaptureSession } from "../types/session.js";
2
+ export declare class SessionStore {
3
+ private readonly sessions;
4
+ get(sessionId: string): CaptureSession | undefined;
5
+ set(session: CaptureSession): void;
6
+ list(): CaptureSession[];
7
+ delete(sessionId: string): boolean;
8
+ }
@@ -0,0 +1,15 @@
1
+ export class SessionStore {
2
+ sessions = new Map();
3
+ get(sessionId) {
4
+ return this.sessions.get(sessionId);
5
+ }
6
+ set(session) {
7
+ this.sessions.set(session.id, session);
8
+ }
9
+ list() {
10
+ return Array.from(this.sessions.values()).sort((left, right) => left.createdAt.localeCompare(right.createdAt));
11
+ }
12
+ delete(sessionId) {
13
+ return this.sessions.delete(sessionId);
14
+ }
15
+ }
@@ -0,0 +1,14 @@
1
+ import type { Logger } from "../logging/logger.js";
2
+ import type { AwaitCaptureSessionInput, AwaitCaptureSessionResult, CaptureSession } from "../types/session.js";
3
+ import { SessionManager } from "./session-manager.js";
4
+ export declare class SessionWaiter {
5
+ private readonly sessionManager;
6
+ private readonly logger;
7
+ private readonly pendingWaits;
8
+ constructor(sessionManager: SessionManager, logger: Logger);
9
+ awaitSession(input: AwaitCaptureSessionInput): Promise<AwaitCaptureSessionResult>;
10
+ notify(session: CaptureSession): void;
11
+ clearSession(sessionId: string): void;
12
+ private toTerminalOutcome;
13
+ private removeWait;
14
+ }
@@ -0,0 +1,102 @@
1
+ export class SessionWaiter {
2
+ sessionManager;
3
+ logger;
4
+ pendingWaits = new Map();
5
+ constructor(sessionManager, logger) {
6
+ this.sessionManager = sessionManager;
7
+ this.logger = logger;
8
+ }
9
+ awaitSession(input) {
10
+ const session = this.sessionManager.getSession(input.sessionId);
11
+ const timeoutMs = input.timeoutMs ?? 30_000;
12
+ if (this.sessionManager.isTerminalStatus(session.status)) {
13
+ return Promise.resolve(this.toTerminalOutcome(session));
14
+ }
15
+ const expiresInMs = Math.max(0, Date.parse(session.expiresAt) - Date.now());
16
+ const waitMs = Math.min(timeoutMs, expiresInMs || timeoutMs);
17
+ return new Promise((resolve) => {
18
+ const pendingWait = {
19
+ resolve,
20
+ timer: setTimeout(() => {
21
+ this.removeWait(session.id, pendingWait);
22
+ const currentSession = this.sessionManager.getSession(session.id);
23
+ if (this.sessionManager.isTerminalStatus(currentSession.status)) {
24
+ resolve(this.toTerminalOutcome(currentSession));
25
+ return;
26
+ }
27
+ resolve({
28
+ outcome: "timed_out",
29
+ session: currentSession,
30
+ waitedMs: timeoutMs
31
+ });
32
+ }, waitMs)
33
+ };
34
+ const pending = this.pendingWaits.get(session.id) ?? new Set();
35
+ pending.add(pendingWait);
36
+ this.pendingWaits.set(session.id, pending);
37
+ this.logger.debug("Registered capture session wait", {
38
+ sessionId: session.id,
39
+ timeoutMs,
40
+ waitMs
41
+ });
42
+ });
43
+ }
44
+ notify(session) {
45
+ if (!this.sessionManager.isTerminalStatus(session.status)) {
46
+ return;
47
+ }
48
+ const pending = this.pendingWaits.get(session.id);
49
+ if (!pending || pending.size === 0) {
50
+ return;
51
+ }
52
+ const outcome = this.toTerminalOutcome(session);
53
+ for (const wait of pending) {
54
+ clearTimeout(wait.timer);
55
+ wait.resolve(outcome);
56
+ }
57
+ this.pendingWaits.delete(session.id);
58
+ this.logger.debug("Resolved capture session waits", {
59
+ sessionId: session.id,
60
+ outcome: outcome.outcome
61
+ });
62
+ }
63
+ clearSession(sessionId) {
64
+ const pending = this.pendingWaits.get(sessionId);
65
+ if (!pending) {
66
+ return;
67
+ }
68
+ for (const wait of pending) {
69
+ clearTimeout(wait.timer);
70
+ }
71
+ this.pendingWaits.delete(sessionId);
72
+ this.logger.debug("Cleared capture session waits", {
73
+ sessionId
74
+ });
75
+ }
76
+ toTerminalOutcome(session) {
77
+ if (session.status === "completed" && session.result) {
78
+ return {
79
+ outcome: "completed",
80
+ session,
81
+ result: session.result
82
+ };
83
+ }
84
+ if (session.status === "cancelled" || session.status === "expired" || session.status === "failed") {
85
+ return {
86
+ outcome: session.status,
87
+ session
88
+ };
89
+ }
90
+ throw new Error(`Expected terminal capture session status, received ${session.status}`);
91
+ }
92
+ removeWait(sessionId, pendingWait) {
93
+ const pending = this.pendingWaits.get(sessionId);
94
+ if (!pending) {
95
+ return;
96
+ }
97
+ pending.delete(pendingWait);
98
+ if (pending.size === 0) {
99
+ this.pendingWaits.delete(sessionId);
100
+ }
101
+ }
102
+ }
@@ -0,0 +1,32 @@
1
+ export type Point = {
2
+ x: number;
3
+ y: number;
4
+ };
5
+ export type RectangleAnnotation = {
6
+ type: "rect";
7
+ x: number;
8
+ y: number;
9
+ width: number;
10
+ height: number;
11
+ label?: string;
12
+ };
13
+ export type ArrowAnnotation = {
14
+ type: "arrow";
15
+ from: Point;
16
+ to: Point;
17
+ label?: string;
18
+ };
19
+ export type TextAnnotation = {
20
+ type: "text";
21
+ x: number;
22
+ y: number;
23
+ text: string;
24
+ };
25
+ export type RedactAnnotation = {
26
+ type: "redact";
27
+ x: number;
28
+ y: number;
29
+ width: number;
30
+ height: number;
31
+ };
32
+ export type Annotation = RectangleAnnotation | ArrowAnnotation | TextAnnotation | RedactAnnotation;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ import type { Annotation } from "./annotation.js";
2
+ export type CaptureCommand = "see" | "clip";
3
+ export type SelectionBounds = {
4
+ x: number;
5
+ y: number;
6
+ width: number;
7
+ height: number;
8
+ };
9
+ export type CaptureImage = {
10
+ mimeType: "image/png";
11
+ bytesBase64: string;
12
+ width: number;
13
+ height: number;
14
+ byteLength: number;
15
+ sourceWidth: number;
16
+ sourceHeight: number;
17
+ backend: string;
18
+ persisted: false;
19
+ };
20
+ export type CaptureContext = {
21
+ activeAppName?: string;
22
+ activeWindowTitle?: string;
23
+ capturedAt: string;
24
+ displayId?: string;
25
+ };
26
+ export type CaptureBundle = {
27
+ sessionId: string;
28
+ command: CaptureCommand;
29
+ image: CaptureImage;
30
+ selection: SelectionBounds;
31
+ annotations: Annotation[];
32
+ context?: CaptureContext;
33
+ };