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.
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/dist/browser/cdp/browser-cdp-discovery-service.d.ts +10 -0
- package/dist/browser/cdp/browser-cdp-discovery-service.js +28 -0
- package/dist/browser/cdp/browser-live-tab-service.d.ts +16 -0
- package/dist/browser/cdp/browser-live-tab-service.js +42 -0
- package/dist/browser/cdp/browser-see-service.d.ts +33 -0
- package/dist/browser/cdp/browser-see-service.js +76 -0
- package/dist/browser/cdp/browser-tab-context-service.d.ts +23 -0
- package/dist/browser/cdp/browser-tab-context-service.js +90 -0
- package/dist/browser/cdp/browser-tab-resolution-service.d.ts +9 -0
- package/dist/browser/cdp/browser-tab-resolution-service.js +65 -0
- package/dist/browser/cdp/browser-tab-screenshot-service.d.ts +20 -0
- package/dist/browser/cdp/browser-tab-screenshot-service.js +59 -0
- package/dist/browser/cdp/cdp-websocket-session.d.ts +9 -0
- package/dist/browser/cdp/cdp-websocket-session.js +99 -0
- package/dist/browser/cdp/chrome-cdp-client.d.ts +12 -0
- package/dist/browser/cdp/chrome-cdp-client.js +141 -0
- package/dist/browser/cdp/live-browser-tab-registry.d.ts +12 -0
- package/dist/browser/cdp/live-browser-tab-registry.js +96 -0
- package/dist/browser/cdp/png-metadata.d.ts +5 -0
- package/dist/browser/cdp/png-metadata.js +16 -0
- package/dist/browser/cdp/tab-model.d.ts +33 -0
- package/dist/browser/cdp/tab-model.js +15 -0
- package/dist/browser/cdp/tab-resolution.d.ts +27 -0
- package/dist/browser/cdp/tab-resolution.js +48 -0
- package/dist/browser/cdp/types.d.ts +71 -0
- package/dist/browser/cdp/types.js +1 -0
- package/dist/capture/capture-pipeline.d.ts +5 -0
- package/dist/capture/capture-pipeline.js +1 -0
- package/dist/capture/create-screen-capture-provider.d.ts +3 -0
- package/dist/capture/create-screen-capture-provider.js +8 -0
- package/dist/capture/in-memory-capture-pipeline.d.ts +13 -0
- package/dist/capture/in-memory-capture-pipeline.js +52 -0
- package/dist/capture/in-memory-image-compositor.d.ts +5 -0
- package/dist/capture/in-memory-image-compositor.js +34 -0
- package/dist/capture/linux-portal-screenshot-provider.d.ts +8 -0
- package/dist/capture/linux-portal-screenshot-provider.js +181 -0
- package/dist/capture/mock-screen-capture-provider.d.ts +5 -0
- package/dist/capture/mock-screen-capture-provider.js +22 -0
- package/dist/capture/png-metadata.d.ts +5 -0
- package/dist/capture/png-metadata.js +18 -0
- package/dist/capture/screen-capture-provider.d.ts +4 -0
- package/dist/capture/screen-capture-provider.js +1 -0
- package/dist/capture/types.d.ts +38 -0
- package/dist/capture/types.js +1 -0
- package/dist/cdp-demo.d.ts +1 -0
- package/dist/cdp-demo.js +41 -0
- package/dist/demo.d.ts +1 -0
- package/dist/demo.js +54 -0
- package/dist/desktop/capture-now.d.ts +1 -0
- package/dist/desktop/capture-now.js +48 -0
- package/dist/desktop/controller.d.ts +25 -0
- package/dist/desktop/controller.js +77 -0
- package/dist/desktop/main.d.ts +1 -0
- package/dist/desktop/main.js +80 -0
- package/dist/desktop/preload.d.ts +1 -0
- package/dist/desktop/preload.js +26 -0
- package/dist/desktop/types.d.ts +31 -0
- package/dist/desktop/types.js +1 -0
- package/dist/errors/app-error.d.ts +7 -0
- package/dist/errors/app-error.js +11 -0
- package/dist/flow/types.d.ts +48 -0
- package/dist/flow/types.js +1 -0
- package/dist/flow/visual-capture-flow.d.ts +13 -0
- package/dist/flow/visual-capture-flow.js +196 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/logging/logger.d.ts +15 -0
- package/dist/logging/logger.js +28 -0
- package/dist/mcp/stdio-server.d.ts +19 -0
- package/dist/mcp/stdio-server.js +272 -0
- package/dist/mcp/tool-registry.d.ts +21 -0
- package/dist/mcp/tool-registry.js +33 -0
- package/dist/mcp-stdio.d.ts +2 -0
- package/dist/mcp-stdio.js +8 -0
- package/dist/overlay/local-overlay-agent.d.ts +46 -0
- package/dist/overlay/local-overlay-agent.js +551 -0
- package/dist/overlay/overlay-bundle-factory.d.ts +4 -0
- package/dist/overlay/overlay-bundle-factory.js +24 -0
- package/dist/overlay/types.d.ts +83 -0
- package/dist/overlay/types.js +1 -0
- package/dist/server.d.ts +19 -0
- package/dist/server.js +158 -0
- package/dist/session/capture-session-service.d.ts +21 -0
- package/dist/session/capture-session-service.js +50 -0
- package/dist/session/session-manager.d.ts +29 -0
- package/dist/session/session-manager.js +217 -0
- package/dist/session/session-store.d.ts +8 -0
- package/dist/session/session-store.js +15 -0
- package/dist/session/session-waiter.d.ts +14 -0
- package/dist/session/session-waiter.js +102 -0
- package/dist/types/annotation.d.ts +32 -0
- package/dist/types/annotation.js +1 -0
- package/dist/types/capture.d.ts +33 -0
- package/dist/types/capture.js +1 -0
- package/dist/types/session.d.ts +36 -0
- package/dist/types/session.js +1 -0
- 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
|
+
};
|