@mind-harness/mh-browser 0.0.0-stage → 0.1.1

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 (51) hide show
  1. package/LICENSE +57 -0
  2. package/README.md +4 -2
  3. package/extension/manifest.json +7 -0
  4. package/extension/sw.js +1 -0
  5. package/icon.svg +5 -0
  6. package/lib/index.js +6194 -0
  7. package/lib/typert.host.d.ts +3 -0
  8. package/lib/typert.host.js +360 -0
  9. package/lib/typert.remote-client.d.ts +24 -0
  10. package/lib/typert.remote-client.js +52 -0
  11. package/lib/types/config.d.ts +20 -0
  12. package/lib/types/index.d.ts +14 -0
  13. package/lib/types/presence/book.d.ts +36 -0
  14. package/lib/types/presence/fold.d.ts +49 -0
  15. package/lib/types/presence/notice.d.ts +8 -0
  16. package/lib/types/presence/plan.d.ts +47 -0
  17. package/lib/types/presence/record.d.ts +47 -0
  18. package/lib/types/presence/register.d.ts +21 -0
  19. package/lib/types/presence/source.d.ts +18 -0
  20. package/lib/types/projections.d.ts +25 -0
  21. package/lib/types/render/envelope.d.ts +80 -0
  22. package/lib/types/representation/agent-view.d.ts +61 -0
  23. package/lib/types/representation/agent-view.js +279 -0
  24. package/lib/types/representation/discovery.d.ts +23 -0
  25. package/lib/types/representation/read.d.ts +45 -0
  26. package/lib/types/service/actions.d.ts +7 -0
  27. package/lib/types/service/browser.d.ts +16 -0
  28. package/lib/types/service/control.d.ts +49 -0
  29. package/lib/types/service/extension.d.ts +30 -0
  30. package/lib/types/service/files.d.ts +17 -0
  31. package/lib/types/service/invoke.d.ts +31 -0
  32. package/lib/types/service/service.d.ts +183 -0
  33. package/lib/types/service/test-hooks.d.ts +7 -0
  34. package/lib/types/service/waits.d.ts +40 -0
  35. package/lib/types/service/windows.d.ts +174 -0
  36. package/lib/types/tools/act.d.ts +4 -0
  37. package/lib/types/tools/attachments.d.ts +23 -0
  38. package/lib/types/tools/delivery.d.ts +64 -0
  39. package/lib/types/tools/download.d.ts +4 -0
  40. package/lib/types/tools/handover.d.ts +4 -0
  41. package/lib/types/tools/invoke.d.ts +4 -0
  42. package/lib/types/tools/open.d.ts +4 -0
  43. package/lib/types/tools/paths.d.ts +3 -0
  44. package/lib/types/tools/presence.d.ts +4 -0
  45. package/lib/types/tools/register.d.ts +3 -0
  46. package/lib/types/tools/session.d.ts +32 -0
  47. package/lib/types/tools/tab-log.d.ts +18 -0
  48. package/lib/types/types.d.ts +172 -0
  49. package/locale/en.json +6 -0
  50. package/locale/zh.json +6 -0
  51. package/package.json +76 -3
@@ -0,0 +1,47 @@
1
+ /** Structured presence record carried on a message source. The model sees the rendered text, not this. */
2
+ export declare const PRESENCE_SOURCES: readonly ["person via sidebar", "cwd reference", "model"];
3
+ export type PresenceSource = (typeof PRESENCE_SOURCES)[number];
4
+ export type PresenceOp = 'present' | 'added' | 'changed' | 'absent' | 'removed';
5
+ export interface PresenceEntryRecord {
6
+ op: PresenceOp;
7
+ url: string;
8
+ source: PresenceSource;
9
+ version?: string;
10
+ observedAt: string;
11
+ digest: string;
12
+ /** The `id` the Agent View declares; `browser_invoke` takes it as well as `url`. */
13
+ id?: string;
14
+ reason?: string;
15
+ }
16
+ export interface PresenceRecord {
17
+ kind: 'baseline' | 'delta';
18
+ replaces?: true;
19
+ entries: PresenceEntryRecord[];
20
+ }
21
+ /** One page in the folded presence set. */
22
+ export interface HeldPage {
23
+ url: string;
24
+ source: PresenceSource;
25
+ state: 'committed' | 'pending';
26
+ order: number;
27
+ seen?: {
28
+ version?: string;
29
+ digest: string;
30
+ observedAt: string;
31
+ id?: string;
32
+ } | {
33
+ absent: string;
34
+ };
35
+ title?: string;
36
+ representation?: 'agent-view' | 'snapshot';
37
+ body?: string;
38
+ agentViewError?: string;
39
+ fragment?: string;
40
+ requires?: string[];
41
+ viewUrl?: string;
42
+ }
43
+ export interface PresenceReceipt {
44
+ url: string;
45
+ receipt: 'added (pending)' | 'already in presence' | 'removed (pending)' | 'not in presence';
46
+ }
47
+ //# sourceMappingURL=record.d.ts.map
@@ -0,0 +1,21 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent';
3
+ import { type UserMessage } from '@deepseek-ai/dsh-llm';
4
+ import type { MhBrowserService } from '../service/service.ts';
5
+ import { type PresenceLogState } from './fold.ts';
6
+ import './source.ts';
7
+ /**
8
+ * Track browser touches, check the pages they named after the step, and merge
9
+ * the reminder after the claimed input. `await next()` runs first, so a
10
+ * caller that puts pages and then returns `next()` is folded into this step.
11
+ */
12
+ export declare function registerPresence(ctx: Context): void;
13
+ /**
14
+ * The reminder this step should carry. A set the log has but this process has
15
+ * not checked (a resumed session) is re-read; one whose reminder a canceled
16
+ * splice discarded is re-announced whole.
17
+ */
18
+ export declare function compose(service: MhBrowserService, agent: Agent, log: PresenceLogState, claimed: readonly UserMessage[], signal?: AbortSignal): Promise<UserMessage | undefined>;
19
+ /** A presence reminder stays pending when the step is rejected or the first step has nothing to say. */
20
+ export declare function mergePresence(agent: Agent, log: PresenceLogState, claimed: readonly UserMessage[], step: number, decision: PreStepDecision, desired: UserMessage | undefined): PreStepDecision;
21
+ //# sourceMappingURL=register.d.ts.map
@@ -0,0 +1,18 @@
1
+ import type { PresenceRecord } from './record.ts';
2
+ declare module '@deepseek-ai/dsh-llm' {
3
+ interface MessageSourceMap {
4
+ /**
5
+ * mh-browser presence reminders and takeover notices; folded by kind.
6
+ * @persistenceAttribution
7
+ */
8
+ 'mh-browser': {
9
+ kind: 'mh-browser';
10
+ presence?: PresenceRecord;
11
+ takeover?: {
12
+ operations: number;
13
+ tabs: string[];
14
+ };
15
+ };
16
+ }
17
+ }
18
+ //# sourceMappingURL=source.d.ts.map
@@ -0,0 +1,25 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { Session } from '@deepseek-ai/dsh-session';
3
+ import { type PresenceLogState } from './presence/fold.ts';
4
+ import { type AttachmentState } from './tools/attachments.ts';
5
+ import { type DeliveryState } from './tools/delivery.ts';
6
+ import { type TabLogState } from './tools/tab-log.ts';
7
+ declare module '@deepseek-ai/dsh-session-projection/types' {
8
+ interface SessionProjectionStateMap {
9
+ mhBrowserDeliveries: DeliveryState;
10
+ mhBrowserPresence: PresenceLogState;
11
+ mhBrowserAttachments: AttachmentState;
12
+ mhBrowserTabs: TabLogState;
13
+ }
14
+ }
15
+ /** Host-only folds over the session log; every read of what the log says goes through these. */
16
+ export declare function registerProjections(ctx: Context): void;
17
+ export interface LogReader {
18
+ deliveries(session: Session): DeliveryState;
19
+ presence(session: Session): PresenceLogState;
20
+ attachments(session: Session): AttachmentState;
21
+ tabs(session: Session): TabLogState;
22
+ }
23
+ /** Fails closed when the registry or a unit is not active. */
24
+ export declare function logReader(ctx: Context): LogReader;
25
+ //# sourceMappingURL=projections.d.ts.map
@@ -0,0 +1,80 @@
1
+ import type { PageError, PageView, TabInfo } from '../types.ts';
2
+ /**
3
+ * Escape a closer a site could use to end a frame the service opened. Only
4
+ * its opening `</name` is rewritten, so a closer cut off before `>` is caught
5
+ * and no site text is dropped.
6
+ */
7
+ export declare function escapeBody(text: string): string;
8
+ /** Escape text the service writes into an attribute. */
9
+ export declare function escapeAttr(text: string): string;
10
+ /** Site text that must stay on one line, such as a title in `<tabs>`. */
11
+ export declare function oneLine(text: string): string;
12
+ export declare function incompleteText(incomplete: {
13
+ load: boolean;
14
+ pendingRequests: number;
15
+ }): string;
16
+ export declare function waitedText(waited: {
17
+ met: 'text' | 'textGone' | 'time' | false;
18
+ seconds: number;
19
+ }): string;
20
+ /**
21
+ * `withDigest` is for a page attached to a tool error: an error result has no
22
+ * meta, so the digest in the header is what records the delivery.
23
+ */
24
+ export declare function renderPage(view: PageView, screenshot?: string, omitBody?: boolean, withDigest?: boolean): string;
25
+ export declare function renderActionResponse(response: {
26
+ name: string;
27
+ status?: number;
28
+ location?: string;
29
+ mediaType?: string;
30
+ body?: string;
31
+ bytes?: number;
32
+ lost?: boolean;
33
+ }): string;
34
+ export declare function renderPageError(error: PageError): string;
35
+ export declare function renderTabs(tabs: TabInfo[], closed: string[]): string;
36
+ export declare function renderDialog(type: string, message: string): string;
37
+ export declare function renderDownload(download: {
38
+ kind: 'saved';
39
+ path: string;
40
+ url: string;
41
+ mediaType?: string;
42
+ bytes: number;
43
+ sha256: string;
44
+ observed: string;
45
+ } | {
46
+ kind: 'error';
47
+ url: string;
48
+ status?: number;
49
+ reason: string;
50
+ }): string;
51
+ export declare function renderAbsent(input: {
52
+ url: string;
53
+ title: string;
54
+ reason: string;
55
+ source?: string;
56
+ declaredBy?: string;
57
+ }): string;
58
+ /**
59
+ * `omitInPresence`: the page's body is the same one presence carries, so a
60
+ * page in presence gets only its attributes.
61
+ */
62
+ export declare function renderOutcome(pages: Array<PageView | PageError>, tabs: TabInfo[], closed: string[], screenshot?: string, omitInPresence?: boolean): string;
63
+ export interface PageReading {
64
+ url: string;
65
+ tab?: string;
66
+ representation: 'agent-view' | 'snapshot';
67
+ version?: string;
68
+ digest?: string;
69
+ /** The `id` the Agent View declares; `browser_invoke` takes it as well as `url`. */
70
+ id?: string;
71
+ incomplete?: string;
72
+ reason?: string;
73
+ }
74
+ /** Reading fields persisted in tool meta. `by` marks the result as this plugin's. */
75
+ export declare function readingsOf(pages: Array<PageView | PageError>, action: string): {
76
+ by: 'mh-browser';
77
+ action: string;
78
+ pages: PageReading[];
79
+ };
80
+ //# sourceMappingURL=envelope.d.ts.map
@@ -0,0 +1,61 @@
1
+ export declare const AGENT_VIEW_VERSION = "0.1";
2
+ export declare const AGENT_VIEW_ACCEPT = "text/markdown; variant=agent-view";
3
+ export interface AgentViewRequest {
4
+ method: string;
5
+ url: string;
6
+ headers?: Record<string, string>;
7
+ body?: Record<string, string>;
8
+ }
9
+ export interface AgentViewAction {
10
+ description: string;
11
+ scope?: 'resource' | 'item';
12
+ request: AgentViewRequest;
13
+ inputSchema?: {
14
+ type: 'object';
15
+ properties: Record<string, {
16
+ type: string;
17
+ description?: string;
18
+ }>;
19
+ required?: string[];
20
+ };
21
+ }
22
+ export interface AgentViewLink {
23
+ rel: string;
24
+ href: string;
25
+ title?: string;
26
+ }
27
+ export interface AgentViewDoc {
28
+ agentView: string;
29
+ id: string;
30
+ version?: string;
31
+ links: AgentViewLink[];
32
+ requires: string[];
33
+ actions: Record<string, AgentViewAction>;
34
+ items: Array<Record<string, unknown>>;
35
+ body: string;
36
+ raw: string;
37
+ }
38
+ export type AgentViewRead = {
39
+ ok: true;
40
+ doc: AgentViewDoc;
41
+ } | {
42
+ ok: false;
43
+ reason: string;
44
+ };
45
+ /**
46
+ * `variant=agent-view` is an Agent View. Another variant is not. Unlabelled
47
+ * `text/markdown` is one only when the body opens with front matter that names
48
+ * `agentView`.
49
+ */
50
+ export declare function isAgentViewContentType(value: string | undefined, body?: string): boolean;
51
+ export declare function readAgentView(text: string, pageUrl: string): AgentViewRead;
52
+ /** The `id` a usable document declares, on the origin it was read from. */
53
+ export declare function agentViewId(text: string, pageUrl: string): string | undefined;
54
+ export declare function parseAgentView(text: string): AgentViewDoc;
55
+ /** Why this document cannot be used. Absent when it can. */
56
+ export declare function unreadableReason(doc: AgentViewDoc, pageUrl: string): string | undefined;
57
+ /** SHA-256 of the document with the front matter `rendered` line removed. */
58
+ export declare function representationDigest(raw: string): string;
59
+ export declare function expandRequest(request: AgentViewRequest, values: Record<string, string>): AgentViewRequest;
60
+ export declare function itemScalars(item: Record<string, unknown>): Record<string, string>;
61
+ //# sourceMappingURL=agent-view.d.ts.map
@@ -0,0 +1,279 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { parse as parseYaml } from 'yaml';
3
+ export const AGENT_VIEW_VERSION = '0.1';
4
+ export const AGENT_VIEW_ACCEPT = 'text/markdown; variant=agent-view';
5
+ const FENCE_OPEN = /```agent-view\b/g;
6
+ const HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'DELETE']);
7
+ const PLACEHOLDER = /\{[^{}]+\}/;
8
+ const YAML_OPTIONS = { maxAliasCount: 50 };
9
+ /**
10
+ * `variant=agent-view` is an Agent View. Another variant is not. Unlabelled
11
+ * `text/markdown` is one only when the body opens with front matter that names
12
+ * `agentView`.
13
+ */
14
+ export function isAgentViewContentType(value, body) {
15
+ if (!value)
16
+ return false;
17
+ const [type, ...params] = value.split(';').map((part) => part.trim().toLowerCase());
18
+ if (type !== 'text/markdown')
19
+ return false;
20
+ const variant = params.find((param) => param.startsWith('variant='));
21
+ if (variant !== undefined) {
22
+ return variant.slice('variant='.length).replace(/"/g, '') === 'agent-view';
23
+ }
24
+ return body !== undefined && hasAgentViewMarker(body);
25
+ }
26
+ export function readAgentView(text, pageUrl) {
27
+ let doc;
28
+ try {
29
+ doc = parseAgentView(text);
30
+ }
31
+ catch (error) {
32
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) };
33
+ }
34
+ const reason = unreadableReason(doc, pageUrl);
35
+ if (reason !== undefined)
36
+ return { ok: false, reason };
37
+ return { ok: true, doc };
38
+ }
39
+ /** The `id` a usable document declares, on the origin it was read from. */
40
+ export function agentViewId(text, pageUrl) {
41
+ const read = readAgentView(text, pageUrl);
42
+ return read.ok ? read.doc.id : undefined;
43
+ }
44
+ export function parseAgentView(text) {
45
+ if (!opensFrontMatter(text)) {
46
+ throw new Error('front matter is missing');
47
+ }
48
+ const end = frontMatterEnd(text);
49
+ if (end < 0)
50
+ throw new Error('front matter is not closed');
51
+ const frontText = text.slice(4, end).trim();
52
+ const body = text.slice(end + 4).replace(/^[ \t]*\r?\n?/, '');
53
+ const front = parseYaml(frontText, YAML_OPTIONS);
54
+ if (front === null || typeof front !== 'object' || Array.isArray(front)) {
55
+ throw new Error('front matter must be a mapping');
56
+ }
57
+ const record = front;
58
+ const actions = normalizeActions(record.actions);
59
+ const items = [];
60
+ for (const match of body.matchAll(/```agent-view\s*\n([\s\S]*?)```/g)) {
61
+ const inner = (match[1] ?? '').trim();
62
+ const parsed = parseYaml(inner, YAML_OPTIONS);
63
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
64
+ items.push(parsed);
65
+ }
66
+ }
67
+ const fenceCount = [...text.matchAll(FENCE_OPEN)].length;
68
+ if (fenceCount !== items.length) {
69
+ throw new Error('agent-view fence count does not match item blocks');
70
+ }
71
+ const doc = {
72
+ agentView: String(record.agentView ?? ''),
73
+ id: String(record.id ?? ''),
74
+ links: normalizeLinks(record.links),
75
+ requires: Array.isArray(record.requires) ? record.requires.map((item) => String(item)) : [],
76
+ actions,
77
+ items,
78
+ body,
79
+ raw: text,
80
+ };
81
+ if (record.version !== undefined)
82
+ doc.version = String(record.version);
83
+ return doc;
84
+ }
85
+ /** Why this document cannot be used. Absent when it can. */
86
+ export function unreadableReason(doc, pageUrl) {
87
+ if (doc.agentView !== AGENT_VIEW_VERSION)
88
+ return 'agentView must be "0.1"';
89
+ let page;
90
+ try {
91
+ page = new URL(pageUrl);
92
+ }
93
+ catch {
94
+ return 'page address is not a URL';
95
+ }
96
+ if (!isAbsoluteHttp(doc.id))
97
+ return `id must be an absolute http(s) URL: ${doc.id}`;
98
+ if (new URL(doc.id).origin !== page.origin)
99
+ return 'id is on another origin';
100
+ for (const [name, action] of Object.entries(doc.actions)) {
101
+ const method = action.request.method.toUpperCase();
102
+ if (!HTTP_METHODS.has(method))
103
+ return `action ${name} has method ${method || 'missing'}`;
104
+ if ((action.scope ?? 'resource') === 'resource' && requestHasPlaceholder(action.request)) {
105
+ return `action ${name} request has a placeholder`;
106
+ }
107
+ }
108
+ const itemActions = Object.entries(doc.actions).filter(([, action]) => (action.scope ?? 'resource') === 'item');
109
+ for (const item of doc.items) {
110
+ if (Number(item.status) === 410)
111
+ continue;
112
+ for (const [name] of itemActions) {
113
+ const count = answerCount(item[name]);
114
+ if (count === 0)
115
+ return `item ${String(item.id)} has no answer for ${name}`;
116
+ if (count !== 1)
117
+ return `item ${String(item.id)} has more than one answer for ${name}`;
118
+ }
119
+ }
120
+ return undefined;
121
+ }
122
+ /** SHA-256 of the document with the front matter `rendered` line removed. */
123
+ export function representationDigest(raw) {
124
+ return createHash('sha256').update(stripRendered(raw)).digest('hex');
125
+ }
126
+ export function expandRequest(request, values) {
127
+ return {
128
+ method: request.method,
129
+ url: expandPlaceholders(request.url, values),
130
+ ...(request.headers
131
+ ? {
132
+ headers: Object.fromEntries(Object.entries(request.headers).map(([name, value]) => [
133
+ name,
134
+ expandPlaceholders(value, values),
135
+ ])),
136
+ }
137
+ : {}),
138
+ ...(request.body
139
+ ? {
140
+ body: Object.fromEntries(Object.entries(request.body).map(([name, value]) => [
141
+ name,
142
+ expandPlaceholders(value, values),
143
+ ])),
144
+ }
145
+ : {}),
146
+ };
147
+ }
148
+ export function itemScalars(item) {
149
+ const out = {};
150
+ for (const [key, value] of Object.entries(item)) {
151
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
152
+ out[key] = String(value);
153
+ }
154
+ }
155
+ return out;
156
+ }
157
+ function expandPlaceholders(template, values) {
158
+ return template.replace(/\{([^{}]+)\}/g, (_, key) => {
159
+ const value = values[key];
160
+ if (value === undefined || value === '')
161
+ throw new Error(`missing value for {${key}}`);
162
+ return value;
163
+ });
164
+ }
165
+ function opensFrontMatter(text) {
166
+ return /^---[ \t]*\r?\n/.test(text);
167
+ }
168
+ /** Index of the newline before the closing `---` line; a line such as `----` does not close it. */
169
+ function frontMatterEnd(text) {
170
+ const close = /\n---[ \t]*(?:\r?\n|$)/g;
171
+ close.lastIndex = 3;
172
+ return close.exec(text)?.index ?? -1;
173
+ }
174
+ function hasAgentViewMarker(body) {
175
+ if (!opensFrontMatter(body))
176
+ return false;
177
+ const end = frontMatterEnd(body);
178
+ if (end < 0)
179
+ return false;
180
+ return /^agentView:/m.test(body.slice(0, end));
181
+ }
182
+ function stripRendered(raw) {
183
+ if (!opensFrontMatter(raw))
184
+ return raw;
185
+ const end = frontMatterEnd(raw);
186
+ if (end < 0)
187
+ return raw;
188
+ const front = raw
189
+ .slice(0, end)
190
+ .split('\n')
191
+ .filter((line) => !line.startsWith('rendered:'))
192
+ .join('\n');
193
+ return front + raw.slice(end);
194
+ }
195
+ function requestHasPlaceholder(request) {
196
+ if (PLACEHOLDER.test(request.url))
197
+ return true;
198
+ for (const value of Object.values(request.headers ?? {})) {
199
+ if (PLACEHOLDER.test(value))
200
+ return true;
201
+ }
202
+ for (const value of Object.values(request.body ?? {})) {
203
+ if (PLACEHOLDER.test(value))
204
+ return true;
205
+ }
206
+ return false;
207
+ }
208
+ function answerCount(value) {
209
+ if (value === true || value === 'yes')
210
+ return 1;
211
+ if (Array.isArray(value))
212
+ return value.length;
213
+ if (value && typeof value === 'object' && 'no' in value)
214
+ return Object.keys(value).length;
215
+ return 0;
216
+ }
217
+ function normalizeLinks(value) {
218
+ if (!Array.isArray(value))
219
+ return [];
220
+ const out = [];
221
+ for (const item of value) {
222
+ if (!item || typeof item !== 'object')
223
+ continue;
224
+ const row = item;
225
+ if (typeof row.rel === 'string' && typeof row.href === 'string') {
226
+ out.push({
227
+ rel: row.rel,
228
+ href: row.href,
229
+ ...(typeof row.title === 'string' ? { title: row.title } : {}),
230
+ });
231
+ }
232
+ }
233
+ return out;
234
+ }
235
+ function normalizeActions(value) {
236
+ if (!value || typeof value !== 'object' || Array.isArray(value))
237
+ return {};
238
+ const out = {};
239
+ for (const [name, raw] of Object.entries(value)) {
240
+ if (!raw || typeof raw !== 'object')
241
+ continue;
242
+ const row = raw;
243
+ const requestRaw = (row.request ?? {});
244
+ const request = {
245
+ method: String(requestRaw.method ?? '').toUpperCase(),
246
+ url: String(requestRaw.url ?? ''),
247
+ };
248
+ if (requestRaw.headers && typeof requestRaw.headers === 'object') {
249
+ request.headers = Object.fromEntries(Object.entries(requestRaw.headers).map(([key, item]) => [
250
+ key,
251
+ String(item),
252
+ ]));
253
+ }
254
+ if (requestRaw.body && typeof requestRaw.body === 'object' && !Array.isArray(requestRaw.body)) {
255
+ request.body = Object.fromEntries(Object.entries(requestRaw.body).map(([key, item]) => [
256
+ key,
257
+ String(item),
258
+ ]));
259
+ }
260
+ const action = { description: String(row.description ?? name), request };
261
+ if (row.scope === 'item' || row.scope === 'resource')
262
+ action.scope = row.scope;
263
+ if (row.inputSchema && typeof row.inputSchema === 'object') {
264
+ action.inputSchema = row.inputSchema;
265
+ }
266
+ out[name] = action;
267
+ }
268
+ return out;
269
+ }
270
+ function isAbsoluteHttp(uri) {
271
+ try {
272
+ const parsed = new URL(uri);
273
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:';
274
+ }
275
+ catch {
276
+ return false;
277
+ }
278
+ }
279
+ //# sourceMappingURL=agent-view.js.map
@@ -0,0 +1,23 @@
1
+ export interface DeclaredLink {
2
+ rel: string;
3
+ href: string;
4
+ title?: string;
5
+ }
6
+ export type AgentViewDeclaration = {
7
+ kind: 'href';
8
+ href: string;
9
+ title?: string;
10
+ } | {
11
+ kind: 'cross-origin';
12
+ href: string;
13
+ };
14
+ /**
15
+ * The first `rel=agent-view` wins. Header links come before links in HTML.
16
+ * A cross-origin href is not fetched and does not fall through to a later one.
17
+ */
18
+ export declare function chooseAgentView(pageUrl: string, header: string | undefined, dom: DeclaredLink[]): AgentViewDeclaration | undefined;
19
+ export declare function samePage(left: string, right: string): boolean;
20
+ /** RFC 8288, including comma-separated values and `title*` (RFC 8187). */
21
+ export declare function parseLinkHeader(header: string | undefined, base: string): DeclaredLink[];
22
+ export declare function linksInHtml(html: string, base: string): DeclaredLink[];
23
+ //# sourceMappingURL=discovery.d.ts.map
@@ -0,0 +1,45 @@
1
+ import type { FetchResponse } from '../types.ts';
2
+ import { type DeclaredLink } from './discovery.ts';
3
+ export interface Shown {
4
+ pageUrl: string;
5
+ fragment?: string;
6
+ href?: string;
7
+ etag?: string;
8
+ raw?: string;
9
+ version?: string;
10
+ digest: string;
11
+ kind: 'agent-view' | 'snapshot';
12
+ text: string;
13
+ snapshot: string;
14
+ agentViewError?: string;
15
+ status?: number;
16
+ mediaType?: string;
17
+ /** Response text of a textual non-HTML document navigation, shown in place of the snapshot. */
18
+ body?: string;
19
+ }
20
+ export interface MainDocument {
21
+ status: number;
22
+ mediaType?: string;
23
+ finalUrl: string;
24
+ link?: string;
25
+ body: string;
26
+ }
27
+ type FetchText = (url: string, accept: string, ifNoneMatch?: string) => Promise<FetchResponse>;
28
+ /**
29
+ * Decide what a tab is showing. A document navigation (`main` given) is
30
+ * discovered again from its response. A relocated page is read from a fresh
31
+ * HTML response, not from the live DOM. A conditional read keeps the
32
+ * previous document when the server answers 304.
33
+ */
34
+ export declare function loadShown(input: {
35
+ pageUrl: string;
36
+ fragment?: string;
37
+ snapshot: string;
38
+ main?: MainDocument;
39
+ domLinks?: DeclaredLink[];
40
+ previous?: Shown;
41
+ relocate: boolean;
42
+ fetch: FetchText;
43
+ }): Promise<Shown>;
44
+ export {};
45
+ //# sourceMappingURL=read.d.ts.map
@@ -0,0 +1,7 @@
1
+ import type { Page } from 'playwright';
2
+ import type { ActAction } from '../types.ts';
3
+ /** Run one snapshot action. Dialog, wait, and look are handled by the service. */
4
+ export declare function runAction(page: Page, action: Exclude<ActAction, {
5
+ kind: 'dialog' | 'wait' | 'look';
6
+ }>, signal: AbortSignal | undefined): Promise<boolean>;
7
+ //# sourceMappingURL=actions.d.ts.map
@@ -0,0 +1,16 @@
1
+ import type { BrowserContext } from 'playwright';
2
+ import type { BrowserName } from '../types.ts';
3
+ export interface LaunchOptions {
4
+ browser: BrowserName;
5
+ headless: boolean;
6
+ profileDir: string;
7
+ /** Test hook: point at a missing executable to exercise the not-found message. */
8
+ executablePath?: string;
9
+ }
10
+ /**
11
+ * One persistent context for the whole process. `chromium` omits `channel`
12
+ * so Playwright uses its own browser; the others use a local install.
13
+ * A missing browser is reported and not downloaded.
14
+ */
15
+ export declare function launchBrowser(options: LaunchOptions): Promise<BrowserContext>;
16
+ //# sourceMappingURL=browser.d.ts.map
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Who holds the browser for one session. Person input outside a service
3
+ * dispatch moves it to the person and aborts the action still waiting.
4
+ * A model handover keeps the hold until hand-back or a stopped call.
5
+ */
6
+ export declare class Control {
7
+ private readonly who;
8
+ private readonly holding;
9
+ private readonly inflight;
10
+ private readonly operations;
11
+ private readonly tabs;
12
+ private readonly since;
13
+ private readonly named;
14
+ of(owner: string): 'model' | 'person';
15
+ /** Abort this session's waiting action when the caller aborts or the person takes over. */
16
+ link(owner: string, caller: AbortSignal | undefined): {
17
+ signal: AbortSignal;
18
+ release(): void;
19
+ };
20
+ /**
21
+ * One person action on a session tab. The person's own window is ignored.
22
+ * Events that arrive while the service is dispatching to this page never
23
+ * reach here.
24
+ */
25
+ note(owner: string, tab: string | undefined): void;
26
+ /** A handover is waiting for the person to hand the browser back. */
27
+ holds(owner: string): boolean;
28
+ /** The tab the handover named, if it named one. */
29
+ namedTab(owner: string): string | undefined;
30
+ /** The model hands the browser to the person and waits for the card. */
31
+ handover(owner: string, tab: string | undefined): void;
32
+ /** The person is done. The caller decides whether a step is still open. */
33
+ finish(owner: string): Taken | undefined;
34
+ /** Stop a handover the model did not finish. Control returns to the model. */
35
+ releaseIfHeld(owner: string): boolean;
36
+ /** Between turns, the person's next message is the hand-back. A waiting card is not. */
37
+ takePerson(owner: string): Taken | undefined;
38
+ /** The Session is gone. Its in-flight calls are aborted. */
39
+ forget(owner: string): void;
40
+ private clear;
41
+ }
42
+ export interface Taken {
43
+ operations: number;
44
+ tabs: string[];
45
+ from: string;
46
+ to: string;
47
+ tab?: string;
48
+ }
49
+ //# sourceMappingURL=control.d.ts.map