@itmetsam/samurai-framework 0.22.2 → 0.22.4

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 (76) hide show
  1. package/AGENT-GUIDE.md +50 -0
  2. package/bin/samurai.mjs +2 -0
  3. package/dist/agent/index.d.ts +11 -0
  4. package/dist/agent/index.js +12 -0
  5. package/dist/agent/instructions.d.ts +13 -0
  6. package/dist/agent/instructions.js +44 -0
  7. package/dist/agent/limits.d.ts +12 -0
  8. package/dist/agent/limits.js +18 -0
  9. package/dist/agent/loop.d.ts +32 -0
  10. package/dist/agent/loop.js +46 -0
  11. package/dist/agent/model.d.ts +9 -0
  12. package/dist/agent/model.js +14 -0
  13. package/dist/agent/prompt.d.ts +15 -0
  14. package/dist/agent/prompt.js +46 -0
  15. package/dist/agent/providers.d.ts +19 -0
  16. package/dist/agent/providers.js +95 -0
  17. package/dist/agent/recording.d.ts +41 -0
  18. package/dist/agent/recording.js +68 -0
  19. package/dist/agent/run.d.ts +11 -0
  20. package/dist/agent/run.js +209 -0
  21. package/dist/agent/sdk-tools.d.ts +15 -0
  22. package/dist/agent/sdk-tools.js +44 -0
  23. package/dist/agent/session.d.ts +62 -0
  24. package/dist/agent/session.js +196 -0
  25. package/dist/agent/snapshot-script.d.ts +18 -0
  26. package/dist/agent/snapshot-script.js +163 -0
  27. package/dist/agent/snapshot.d.ts +54 -0
  28. package/dist/agent/snapshot.js +95 -0
  29. package/dist/agent/take-over.d.ts +19 -0
  30. package/dist/agent/take-over.js +49 -0
  31. package/dist/agent/tools/act.d.ts +2 -0
  32. package/dist/agent/tools/act.js +124 -0
  33. package/dist/agent/tools/ask.d.ts +1 -0
  34. package/dist/agent/tools/ask.js +19 -0
  35. package/dist/agent/tools/context.d.ts +63 -0
  36. package/dist/agent/tools/context.js +42 -0
  37. package/dist/agent/tools/expect.d.ts +1 -0
  38. package/dist/agent/tools/expect.js +152 -0
  39. package/dist/agent/tools/finish.d.ts +1 -0
  40. package/dist/agent/tools/finish.js +12 -0
  41. package/dist/agent/tools/index.d.ts +3 -0
  42. package/dist/agent/tools/index.js +21 -0
  43. package/dist/agent/tools/navigate.d.ts +1 -0
  44. package/dist/agent/tools/navigate.js +53 -0
  45. package/dist/agent/tools/origin-guard.d.ts +6 -0
  46. package/dist/agent/tools/origin-guard.js +19 -0
  47. package/dist/agent/tools/origins.d.ts +11 -0
  48. package/dist/agent/tools/origins.js +22 -0
  49. package/dist/agent/tools/screenshot.d.ts +1 -0
  50. package/dist/agent/tools/screenshot.js +39 -0
  51. package/dist/agent/tools/snapshot.d.ts +1 -0
  52. package/dist/agent/tools/snapshot.js +12 -0
  53. package/dist/agent/tools/use-secret.d.ts +1 -0
  54. package/dist/agent/tools/use-secret.js +41 -0
  55. package/dist/agent/types.d.ts +104 -0
  56. package/dist/agent/types.js +1 -0
  57. package/dist/capabilities.d.ts +2 -0
  58. package/dist/capabilities.js +2 -0
  59. package/dist/cli/args.d.ts +5 -0
  60. package/dist/cli/args.js +5 -0
  61. package/dist/cli/generate.d.ts +10 -0
  62. package/dist/cli/generate.js +139 -0
  63. package/dist/cli/io.d.ts +2 -0
  64. package/dist/cli/main.js +3 -0
  65. package/dist/cli/prompt.d.ts +2 -0
  66. package/dist/cli/prompt.js +16 -0
  67. package/dist/cli/record.js +3 -1
  68. package/dist/cli/usage.d.ts +1 -1
  69. package/dist/cli/usage.js +19 -2
  70. package/dist/index.js +2 -0
  71. package/dist/recorder/normalise.d.ts +6 -0
  72. package/dist/recorder/normalise.js +2 -0
  73. package/dist/recorder/session.d.ts +1 -1
  74. package/dist/recorder/session.js +1 -1
  75. package/dist/types/config.d.ts +5 -0
  76. package/package.json +19 -4
@@ -0,0 +1,209 @@
1
+ import { loadRawConfig, projectDir } from "../config/config.js";
2
+ import { maskDeep, maskText } from "../config/mask.js";
3
+ import { launchRecordingBrowser, prepareRecordingBrowser, } from "../recorder/session-browser.js";
4
+ import { resultSecrets } from "../recorder/session-secrets.js";
5
+ import { fixtureCatalog } from "../runner/fixture-registry.js";
6
+ import { withProject } from "../runner/project.js";
7
+ import { loadInstructions } from "./instructions.js";
8
+ import { resolveLimits } from "./limits.js";
9
+ import { runLoop } from "./loop.js";
10
+ import { assertModelObject } from "./model.js";
11
+ import { buildSystemPrompt } from "./prompt.js";
12
+ import { StepRecorder } from "./recording.js";
13
+ import { toSdkTools } from "./sdk-tools.js";
14
+ import { AgentController } from "./session.js";
15
+ import { Snapshotter } from "./snapshot.js";
16
+ import { createTools } from "./tools/index.js";
17
+ import { OriginPolicy } from "./tools/origins.js";
18
+ import { personRecording } from "./take-over.js";
19
+ const ACTION_TIMEOUT = 5000;
20
+ async function loadAi(deps) {
21
+ try {
22
+ return await (deps.importAi ?? (() => import("ai")))();
23
+ }
24
+ catch (error) {
25
+ const missing = error.code;
26
+ throw new Error(missing === "ERR_MODULE_NOT_FOUND" || deps.importAi
27
+ ? 'agent(): the "ai" package is not installed. Install it, and a provider package for your model, with: npm install ai zod @ai-sdk/anthropic'
28
+ : `agent(): could not load the "ai" package: ${error.message}`);
29
+ }
30
+ }
31
+ /** The fixtures of the project's config, for the prompt; the agent works without them */
32
+ async function fixturesOf(options) {
33
+ try {
34
+ const config = options.config ?? (await loadRawConfig());
35
+ return fixtureCatalog(config.fixtures ?? []);
36
+ }
37
+ catch {
38
+ return [];
39
+ }
40
+ }
41
+ function endReason(controller, tally, limit, failed) {
42
+ const stopped = controller.stopReason;
43
+ if (stopped)
44
+ return stopped;
45
+ if (failed)
46
+ return "error";
47
+ if (tally.finished)
48
+ return "finished";
49
+ return limit ?? "finished";
50
+ }
51
+ /** Starts a run; its work goes on in the background and `result` settles when it ends */
52
+ export function startAgent(options, deps = {}) {
53
+ assertModelObject(options.model);
54
+ const limits = resolveLimits(options.limits);
55
+ const person = {};
56
+ const controller = new AgentController({
57
+ begin: () => person.hooks?.begin() ?? Promise.resolve(),
58
+ end: () => person.hooks?.end() ?? Promise.resolve(""),
59
+ }, options.onEvent);
60
+ options.signal?.addEventListener("abort", () => controller.stop(), {
61
+ once: true,
62
+ });
63
+ if (options.signal?.aborted)
64
+ controller.stop();
65
+ const tally = { modelCalls: 0, tokens: 0, finished: false };
66
+ const done = { text: "" };
67
+ const work = async () => {
68
+ const ai = await loadAi(deps);
69
+ await withProject(options, async (run) => {
70
+ const { settings } = run;
71
+ const instructions = loadInstructions(projectDir());
72
+ const startUrl = options.url ?? settings.baseURL;
73
+ const prepared = await prepareRecordingBrowser({
74
+ ...(options.browser && { browser: options.browser }),
75
+ ...(options.port !== undefined && { port: options.port }),
76
+ signal: controller.signal,
77
+ });
78
+ const steps = new StepRecorder({
79
+ file: options.file,
80
+ create: options.create,
81
+ test: options.test,
82
+ baseURL: settings.baseURL,
83
+ onStep: (event, code, source) => controller.emit({
84
+ type: "step",
85
+ event,
86
+ ...(code !== undefined && { code }),
87
+ source,
88
+ }),
89
+ });
90
+ done.steps = steps;
91
+ const { browser, page } = await launchRecordingBrowser(prepared, {
92
+ ...(options.port !== undefined && { port: options.port }),
93
+ signal: controller.signal,
94
+ headless: options.headless ?? false,
95
+ });
96
+ try {
97
+ deps.onPage?.(page);
98
+ if (options.url !== undefined)
99
+ await page.goto(options.url);
100
+ else if (settings.baseURL !== undefined)
101
+ await page.navigateTo(settings.baseURL);
102
+ const current = await page.url();
103
+ const onWeb = /^https?:/.test(current);
104
+ const snapshots = new Snapshotter(page, {
105
+ secretFields: options.secretFields ?? [],
106
+ });
107
+ if (onWeb && steps.count === 0)
108
+ steps.observe({ type: "goto", url: current });
109
+ const origins = new OriginPolicy([
110
+ ...(options.allowedOrigins ?? []),
111
+ ...(onWeb ? [current] : startUrl ? [startUrl] : []),
112
+ ]);
113
+ person.hooks = personRecording({
114
+ page,
115
+ steps,
116
+ snapshots,
117
+ ...(settings.baseURL !== undefined && { baseURL: settings.baseURL }),
118
+ });
119
+ const context = {
120
+ page,
121
+ snapshots,
122
+ steps,
123
+ origins,
124
+ secrets: {
125
+ allowed: options.secrets ?? [],
126
+ get: (name) => run.secrets.get(name),
127
+ },
128
+ vision: options.vision === true,
129
+ actionTimeout: ACTION_TIMEOUT,
130
+ ask: (question, choices) => controller.ask(question, choices),
131
+ finish: (summary) => {
132
+ done.summary = summary;
133
+ tally.finished = true;
134
+ },
135
+ emit: (event) => controller.emit(event),
136
+ secretOnScreen: false,
137
+ };
138
+ const system = buildSystemPrompt({
139
+ instructions: instructions.text,
140
+ fixtures: await fixturesOf(options),
141
+ secrets: options.secrets ?? [],
142
+ origins: origins.list(),
143
+ vision: options.vision === true,
144
+ });
145
+ const first = onWeb
146
+ ? `The browser is open on ${current}. This is the page now:\n\n${maskText((await snapshots.take()).text)}`
147
+ : "No page is open yet: start with navigate.";
148
+ const tools = toSdkTools(ai, createTools(context), {
149
+ wait: () => controller.gate(),
150
+ note: async () => controller.takeNote(),
151
+ });
152
+ const outcome = await runLoop({
153
+ ai,
154
+ model: options.model,
155
+ system,
156
+ prompt: `Goal: ${options.goal}\n\n${first}`,
157
+ tools,
158
+ controller,
159
+ limits,
160
+ tally,
161
+ });
162
+ done.text = outcome.text;
163
+ if (outcome.limit)
164
+ done.limit = outcome.limit;
165
+ }
166
+ finally {
167
+ await person.hooks?.dispose().catch(() => undefined);
168
+ await browser.close().catch(() => undefined);
169
+ }
170
+ });
171
+ };
172
+ const stopTimer = controller.watchTime(limits.timeoutMs);
173
+ const result = work()
174
+ .then(() => false)
175
+ .catch((error) => {
176
+ done.error = maskText(error instanceof Error ? error.message : String(error));
177
+ return true;
178
+ })
179
+ .then(async (failed) => {
180
+ stopTimer();
181
+ await controller.settle();
182
+ const steps = done.steps;
183
+ const reason = endReason(controller, tally, done.limit, failed);
184
+ const summary = done.summary ?? (done.text.trim() || undefined);
185
+ const ended = maskDeep({
186
+ reason,
187
+ ...(reason === "finished" && summary !== undefined && { summary }),
188
+ ...(reason === "error" &&
189
+ done.error !== undefined && { error: done.error }),
190
+ file: steps?.file ?? options.file,
191
+ test: steps?.testName ?? "",
192
+ steps: steps?.count ?? 0,
193
+ secrets: steps ? resultSecrets(steps.events, []) : [],
194
+ tokens: tally.tokens,
195
+ });
196
+ controller.end(ended);
197
+ return ended;
198
+ });
199
+ return {
200
+ events: controller.events,
201
+ result,
202
+ answer: (id, text) => controller.answer(id, text),
203
+ pause: () => controller.pause(),
204
+ resume: () => controller.resume(),
205
+ takeOver: () => controller.takeOver(),
206
+ handBack: () => controller.handBack(),
207
+ stop: () => controller.stop(),
208
+ };
209
+ }
@@ -0,0 +1,15 @@
1
+ import type { AgentTool } from "./tools/context.js";
2
+ /** The AI SDK, loaded when a run starts: it is an optional peer dependency */
3
+ export type AiModule = typeof import("ai");
4
+ /** What stands between the model's call and the tool: waiting while paused, and the note after a hand back */
5
+ export interface ToolGate {
6
+ /** Resolves when the agent may act; rejects when the run ended */
7
+ wait(): Promise<void>;
8
+ /** Text to give the model instead of running its call, once, after the person used the browser */
9
+ note(): Promise<string | undefined>;
10
+ }
11
+ /**
12
+ * The tools in the AI SDK's shape. The SDK is given the JSON schema only: it would answer a wrong call with a
13
+ * dump of the validation library's issues, so the tools validate themselves and answer in a sentence.
14
+ */
15
+ export declare function toSdkTools(ai: AiModule, tools: readonly AgentTool[], gate: ToolGate): Record<string, unknown>;
@@ -0,0 +1,44 @@
1
+ function modelOutput(result) {
2
+ if (!result.ok)
3
+ return { type: "error-text", value: result.error };
4
+ if (!result.image)
5
+ return { type: "text", value: result.message };
6
+ return {
7
+ type: "content",
8
+ value: [
9
+ { type: "text", text: result.message },
10
+ {
11
+ type: "file",
12
+ data: { type: "data", data: result.image.data },
13
+ mediaType: result.image.mediaType,
14
+ },
15
+ ],
16
+ };
17
+ }
18
+ /**
19
+ * The tools in the AI SDK's shape. The SDK is given the JSON schema only: it would answer a wrong call with a
20
+ * dump of the validation library's issues, so the tools validate themselves and answer in a sentence.
21
+ */
22
+ export function toSdkTools(ai, tools, gate) {
23
+ const entries = tools.map((agentTool) => {
24
+ const schema = ai.jsonSchema(() => ai.zodSchema(agentTool.inputSchema).jsonSchema, {
25
+ validate: (value) => ({ success: true, value }),
26
+ });
27
+ return [
28
+ agentTool.name,
29
+ ai.tool({
30
+ description: agentTool.description,
31
+ inputSchema: schema,
32
+ execute: async (input) => {
33
+ await gate.wait();
34
+ const note = await gate.note();
35
+ if (note !== undefined)
36
+ return { ok: false, error: note };
37
+ return agentTool.run(input);
38
+ },
39
+ toModelOutput: ({ output }) => modelOutput(output),
40
+ }),
41
+ ];
42
+ });
43
+ return Object.fromEntries(entries);
44
+ }
@@ -0,0 +1,62 @@
1
+ import type { AgentEvent, AgentResult } from "./types.js";
2
+ export type AbortReason = "stopped" | "time-limit" | "token-limit";
3
+ /** What taking the browser over starts and ends: the recording of what the person does */
4
+ export interface TakeOverHooks {
5
+ begin(): Promise<void>;
6
+ /** Ends the recording; the text tells the agent what the person did */
7
+ end(): Promise<string>;
8
+ }
9
+ /**
10
+ * The state of one run that the person controls: the event stream, pause and take over, the open questions, stop,
11
+ * and the active time. Time spent paused, taken over or waiting for an answer does not count against the time
12
+ * limit: it is the person's time, not the agent's.
13
+ */
14
+ export declare class AgentController {
15
+ private readonly hooks;
16
+ private readonly onEvent?;
17
+ private readonly log;
18
+ private readonly readers;
19
+ private closed;
20
+ private mode;
21
+ private transition;
22
+ private readonly gateWaiters;
23
+ private readonly aborter;
24
+ private abortReason;
25
+ private readonly questions;
26
+ private asked;
27
+ private note;
28
+ private readonly startedAt;
29
+ private idleSince;
30
+ private idleTotal;
31
+ constructor(hooks: TakeOverHooks, onEvent?: ((event: AgentEvent) => void) | undefined);
32
+ /** Aborts when the run is stopped or a limit is reached */
33
+ get signal(): AbortSignal;
34
+ get stopReason(): AbortReason | undefined;
35
+ get events(): AsyncIterable<AgentEvent>;
36
+ private read;
37
+ emit(event: AgentEvent): void;
38
+ /** Emits `ended` once; later calls change nothing */
39
+ end(result: AgentResult): void;
40
+ /** Time the agent has been working, in ms */
41
+ activeMs(now?: number): number;
42
+ private touch;
43
+ private wake;
44
+ /** Resolves when the agent may act; rejects when the run was stopped or hit a limit */
45
+ gate(): Promise<void>;
46
+ pause(): void;
47
+ resume(): void;
48
+ takeOver(): void;
49
+ handBack(): void;
50
+ /** The note for the agent about what the person did, once, after a hand back */
51
+ takeNote(): string | undefined;
52
+ /** Waits for a hand back that is still ending the person's recording (the run is ending anyway) */
53
+ settle(): Promise<void>;
54
+ /** Asks the person; waits without counting against the time limit. Rejects when the run stops */
55
+ ask(question: string, options?: string[]): Promise<string>;
56
+ answer(id: string, text: string): void;
57
+ /** Ends the run early: `stop()` from the person, or a limit */
58
+ abort(reason: AbortReason): void;
59
+ stop(): void;
60
+ /** Aborts with `time-limit` once the active time passes `timeoutMs`. Returns the function that stops the check */
61
+ watchTime(timeoutMs: number): () => void;
62
+ }
@@ -0,0 +1,196 @@
1
+ import { maskDeep } from "../config/mask.js";
2
+ /**
3
+ * The state of one run that the person controls: the event stream, pause and take over, the open questions, stop,
4
+ * and the active time. Time spent paused, taken over or waiting for an answer does not count against the time
5
+ * limit: it is the person's time, not the agent's.
6
+ */
7
+ export class AgentController {
8
+ constructor(hooks, onEvent) {
9
+ this.hooks = hooks;
10
+ this.onEvent = onEvent;
11
+ this.log = [];
12
+ this.readers = new Set();
13
+ this.closed = false;
14
+ this.mode = "running";
15
+ this.transition = Promise.resolve();
16
+ this.gateWaiters = new Set();
17
+ this.aborter = new AbortController();
18
+ this.questions = new Map();
19
+ this.asked = 0;
20
+ this.startedAt = Date.now();
21
+ this.idleTotal = 0;
22
+ }
23
+ /** Aborts when the run is stopped or a limit is reached */
24
+ get signal() {
25
+ return this.aborter.signal;
26
+ }
27
+ get stopReason() {
28
+ return this.abortReason;
29
+ }
30
+ get events() {
31
+ return { [Symbol.asyncIterator]: () => this.read() };
32
+ }
33
+ async *read() {
34
+ for (let index = 0;;) {
35
+ while (index < this.log.length)
36
+ yield this.log[index++];
37
+ if (this.closed)
38
+ return;
39
+ await new Promise((resolve) => {
40
+ const wake = () => {
41
+ this.readers.delete(wake);
42
+ resolve();
43
+ };
44
+ this.readers.add(wake);
45
+ });
46
+ }
47
+ }
48
+ emit(event) {
49
+ if (this.closed)
50
+ return;
51
+ const safe = maskDeep(event);
52
+ this.log.push(safe);
53
+ try {
54
+ this.onEvent?.(safe);
55
+ }
56
+ catch {
57
+ // a failing listener must not stop the run
58
+ }
59
+ for (const wake of [...this.readers])
60
+ wake();
61
+ }
62
+ /** Emits `ended` once; later calls change nothing */
63
+ end(result) {
64
+ if (this.closed)
65
+ return;
66
+ this.emit({ type: "ended", result });
67
+ this.closed = true;
68
+ for (const wake of [...this.readers])
69
+ wake();
70
+ }
71
+ /** Time the agent has been working, in ms */
72
+ activeMs(now = Date.now()) {
73
+ const idle = this.idleSince === undefined ? 0 : now - this.idleSince;
74
+ return now - this.startedAt - this.idleTotal - idle;
75
+ }
76
+ touch() {
77
+ const idle = this.mode !== "running" || this.questions.size > 0;
78
+ const now = Date.now();
79
+ if (idle && this.idleSince === undefined)
80
+ this.idleSince = now;
81
+ if (!idle && this.idleSince !== undefined) {
82
+ this.idleTotal += now - this.idleSince;
83
+ this.idleSince = undefined;
84
+ }
85
+ }
86
+ wake() {
87
+ for (const resume of [...this.gateWaiters])
88
+ resume();
89
+ this.gateWaiters.clear();
90
+ }
91
+ /** Resolves when the agent may act; rejects when the run was stopped or hit a limit */
92
+ async gate() {
93
+ for (;;) {
94
+ this.signal.throwIfAborted();
95
+ await this.transition;
96
+ this.signal.throwIfAborted();
97
+ if (this.mode === "running")
98
+ return;
99
+ await new Promise((resolve) => this.gateWaiters.add(resolve));
100
+ }
101
+ }
102
+ pause() {
103
+ if (this.mode !== "running")
104
+ return;
105
+ this.mode = "paused";
106
+ this.touch();
107
+ }
108
+ resume() {
109
+ if (this.mode !== "paused")
110
+ return;
111
+ this.mode = "running";
112
+ this.touch();
113
+ this.wake();
114
+ }
115
+ takeOver() {
116
+ if (this.mode === "taken-over")
117
+ return;
118
+ this.mode = "taken-over";
119
+ this.touch();
120
+ this.transition = this.transition
121
+ .then(() => this.hooks.begin())
122
+ .catch(() => undefined);
123
+ }
124
+ handBack() {
125
+ if (this.mode !== "taken-over")
126
+ return;
127
+ this.transition = this.transition
128
+ .then(async () => {
129
+ this.note = await this.hooks.end();
130
+ })
131
+ .catch(() => undefined)
132
+ .then(() => {
133
+ this.mode = "running";
134
+ this.touch();
135
+ this.wake();
136
+ });
137
+ }
138
+ /** The note for the agent about what the person did, once, after a hand back */
139
+ takeNote() {
140
+ const note = this.note;
141
+ this.note = undefined;
142
+ return note;
143
+ }
144
+ /** Waits for a hand back that is still ending the person's recording (the run is ending anyway) */
145
+ settle() {
146
+ return this.transition;
147
+ }
148
+ /** Asks the person; waits without counting against the time limit. Rejects when the run stops */
149
+ ask(question, options) {
150
+ const id = `q${++this.asked}`;
151
+ return new Promise((resolve, reject) => {
152
+ this.questions.set(id, {
153
+ answer: (text) => resolve(text),
154
+ fail: reject,
155
+ });
156
+ this.touch();
157
+ this.emit({
158
+ type: "question",
159
+ id,
160
+ question,
161
+ ...(options && options.length > 0 && { options }),
162
+ });
163
+ }).finally(() => {
164
+ this.questions.delete(id);
165
+ this.touch();
166
+ });
167
+ }
168
+ answer(id, text) {
169
+ const open = this.questions.get(id);
170
+ if (!open)
171
+ throw new Error(`agent(): there is no open question ${id}`);
172
+ open.answer(text);
173
+ }
174
+ /** Ends the run early: `stop()` from the person, or a limit */
175
+ abort(reason) {
176
+ if (this.abortReason)
177
+ return;
178
+ this.abortReason = reason;
179
+ this.aborter.abort(new Error(`agent run ended: ${reason}`));
180
+ for (const open of this.questions.values())
181
+ open.fail(new Error("agent run ended"));
182
+ this.wake();
183
+ }
184
+ stop() {
185
+ this.abort("stopped");
186
+ }
187
+ /** Aborts with `time-limit` once the active time passes `timeoutMs`. Returns the function that stops the check */
188
+ watchTime(timeoutMs) {
189
+ const timer = setInterval(() => {
190
+ if (this.activeMs() >= timeoutMs)
191
+ this.abort("time-limit");
192
+ }, Math.min(250, Math.max(10, timeoutMs / 4)));
193
+ timer.unref();
194
+ return () => clearInterval(timer);
195
+ }
196
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Function declaration, called as `(options)` with `{ maxChars, secretFields }`. Writes the page as text (see
3
+ * `Snapshotter.take`) and remembers the elements it numbered, so `RESOLVE_SCRIPT` can find them again. Secret
4
+ * values are replaced here, in the page: they never reach the framework.
5
+ */
6
+ export declare const SNAPSHOT_SCRIPT: string;
7
+ /**
8
+ * Function declaration, called as `({ ref, focused, secretFields })` with the number of a reference, or `focused`
9
+ * for the element that has focus. Answers `{ status: "ok", target, candidates, secret, dotted, role, name }`, or a status
10
+ * that says why the reference does not resolve: `none` (no snapshot in this document), `unknown`, `older` (it was
11
+ * in an earlier snapshot), `gone` (removed since), `nofocus`.
12
+ */
13
+ export declare const RESOLVE_SCRIPT: string;
14
+ /**
15
+ * Function declaration, called as `({ secretFields })`. Answers the first of the caller's selectors that matches a
16
+ * visible field showing a value in plain text (a password input draws dots and does not count), else "".
17
+ */
18
+ export declare const SECRET_ON_SCREEN_SCRIPT: string;