@silbercue/chrome 0.2.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 (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +229 -0
  3. package/build/cache/a11y-tree.d.ts +252 -0
  4. package/build/cache/a11y-tree.js +1956 -0
  5. package/build/cache/index.d.ts +8 -0
  6. package/build/cache/index.js +4 -0
  7. package/build/cache/selector-cache.d.ts +47 -0
  8. package/build/cache/selector-cache.js +119 -0
  9. package/build/cache/session-defaults.d.ts +27 -0
  10. package/build/cache/session-defaults.js +130 -0
  11. package/build/cache/tab-state-cache.d.ts +39 -0
  12. package/build/cache/tab-state-cache.js +171 -0
  13. package/build/cdp/cdp-client.d.ts +25 -0
  14. package/build/cdp/cdp-client.js +146 -0
  15. package/build/cdp/chrome-launcher.d.ts +85 -0
  16. package/build/cdp/chrome-launcher.js +502 -0
  17. package/build/cdp/console-collector.d.ts +53 -0
  18. package/build/cdp/console-collector.js +147 -0
  19. package/build/cdp/debug.d.ts +1 -0
  20. package/build/cdp/debug.js +6 -0
  21. package/build/cdp/dialog-handler.d.ts +54 -0
  22. package/build/cdp/dialog-handler.js +129 -0
  23. package/build/cdp/dom-watcher.d.ts +45 -0
  24. package/build/cdp/dom-watcher.js +195 -0
  25. package/build/cdp/emulation.d.ts +12 -0
  26. package/build/cdp/emulation.js +17 -0
  27. package/build/cdp/index.d.ts +11 -0
  28. package/build/cdp/index.js +6 -0
  29. package/build/cdp/network-collector.d.ts +77 -0
  30. package/build/cdp/network-collector.js +257 -0
  31. package/build/cdp/protocol.d.ts +20 -0
  32. package/build/cdp/protocol.js +1 -0
  33. package/build/cdp/session-manager.d.ts +62 -0
  34. package/build/cdp/session-manager.js +205 -0
  35. package/build/cdp/settle.d.ts +16 -0
  36. package/build/cdp/settle.js +71 -0
  37. package/build/cli/license-commands.d.ts +19 -0
  38. package/build/cli/license-commands.js +199 -0
  39. package/build/cli/top-level-commands.d.ts +49 -0
  40. package/build/cli/top-level-commands.js +222 -0
  41. package/build/hooks/index.d.ts +2 -0
  42. package/build/hooks/index.js +1 -0
  43. package/build/hooks/pro-hooks.d.ts +126 -0
  44. package/build/hooks/pro-hooks.js +17 -0
  45. package/build/index.d.ts +4 -0
  46. package/build/index.js +86 -0
  47. package/build/license/free-tier-config.d.ts +14 -0
  48. package/build/license/free-tier-config.js +18 -0
  49. package/build/license/index.d.ts +4 -0
  50. package/build/license/index.js +2 -0
  51. package/build/license/license-status.d.ts +15 -0
  52. package/build/license/license-status.js +9 -0
  53. package/build/overlay/session-overlay.d.ts +22 -0
  54. package/build/overlay/session-overlay.js +372 -0
  55. package/build/plan/index.d.ts +7 -0
  56. package/build/plan/index.js +4 -0
  57. package/build/plan/plan-conditions.d.ts +12 -0
  58. package/build/plan/plan-conditions.js +242 -0
  59. package/build/plan/plan-executor.d.ts +49 -0
  60. package/build/plan/plan-executor.js +259 -0
  61. package/build/plan/plan-state-store.d.ts +24 -0
  62. package/build/plan/plan-state-store.js +43 -0
  63. package/build/plan/plan-variables.d.ts +16 -0
  64. package/build/plan/plan-variables.js +71 -0
  65. package/build/registry.d.ts +124 -0
  66. package/build/registry.js +884 -0
  67. package/build/server.d.ts +1 -0
  68. package/build/server.js +245 -0
  69. package/build/tools/click.d.ts +34 -0
  70. package/build/tools/click.js +293 -0
  71. package/build/tools/configure-session.d.ts +15 -0
  72. package/build/tools/configure-session.js +45 -0
  73. package/build/tools/console-logs.d.ts +18 -0
  74. package/build/tools/console-logs.js +44 -0
  75. package/build/tools/dom-snapshot.d.ts +13 -0
  76. package/build/tools/dom-snapshot.js +259 -0
  77. package/build/tools/element-utils.d.ts +23 -0
  78. package/build/tools/element-utils.js +133 -0
  79. package/build/tools/error-utils.d.ts +8 -0
  80. package/build/tools/error-utils.js +27 -0
  81. package/build/tools/evaluate.d.ts +34 -0
  82. package/build/tools/evaluate.js +217 -0
  83. package/build/tools/file-upload.d.ts +20 -0
  84. package/build/tools/file-upload.js +174 -0
  85. package/build/tools/fill-form.d.ts +39 -0
  86. package/build/tools/fill-form.js +256 -0
  87. package/build/tools/handle-dialog.d.ts +15 -0
  88. package/build/tools/handle-dialog.js +48 -0
  89. package/build/tools/index.d.ts +35 -0
  90. package/build/tools/index.js +18 -0
  91. package/build/tools/navigate.d.ts +18 -0
  92. package/build/tools/navigate.js +111 -0
  93. package/build/tools/network-monitor.d.ts +18 -0
  94. package/build/tools/network-monitor.js +66 -0
  95. package/build/tools/observe.d.ts +44 -0
  96. package/build/tools/observe.js +339 -0
  97. package/build/tools/press-key.d.ts +33 -0
  98. package/build/tools/press-key.js +155 -0
  99. package/build/tools/read-page.d.ts +22 -0
  100. package/build/tools/read-page.js +100 -0
  101. package/build/tools/run-plan.d.ts +205 -0
  102. package/build/tools/run-plan.js +215 -0
  103. package/build/tools/screenshot.d.ts +16 -0
  104. package/build/tools/screenshot.js +283 -0
  105. package/build/tools/scroll.d.ts +28 -0
  106. package/build/tools/scroll.js +143 -0
  107. package/build/tools/switch-tab.d.ts +26 -0
  108. package/build/tools/switch-tab.js +355 -0
  109. package/build/tools/tab-status.d.ts +7 -0
  110. package/build/tools/tab-status.js +50 -0
  111. package/build/tools/type.d.ts +31 -0
  112. package/build/tools/type.js +247 -0
  113. package/build/tools/virtual-desk.d.ts +7 -0
  114. package/build/tools/virtual-desk.js +108 -0
  115. package/build/tools/visual-constants.d.ts +3 -0
  116. package/build/tools/visual-constants.js +10 -0
  117. package/build/tools/wait-for.d.ts +26 -0
  118. package/build/tools/wait-for.js +323 -0
  119. package/build/transport/index.d.ts +3 -0
  120. package/build/transport/index.js +2 -0
  121. package/build/transport/pipe-transport.d.ts +18 -0
  122. package/build/transport/pipe-transport.js +63 -0
  123. package/build/transport/transport.d.ts +8 -0
  124. package/build/transport/transport.js +1 -0
  125. package/build/transport/websocket-transport.d.ts +22 -0
  126. package/build/transport/websocket-transport.js +200 -0
  127. package/build/types.d.ts +21 -0
  128. package/build/types.js +1 -0
  129. package/package.json +62 -0
@@ -0,0 +1,242 @@
1
+ // --- Tokenizer ---
2
+ const OPERATORS = ["===", "!==", "==", "!=", ">=", "<=", "&&", "||", ">", "<"];
3
+ function tokenize(expression) {
4
+ const tokens = [];
5
+ let i = 0;
6
+ while (i < expression.length) {
7
+ // Skip whitespace
8
+ if (/\s/.test(expression[i])) {
9
+ i++;
10
+ continue;
11
+ }
12
+ // Parentheses
13
+ if (expression[i] === "(") {
14
+ tokens.push({ type: "lparen", value: "(", raw: "(" });
15
+ i++;
16
+ continue;
17
+ }
18
+ if (expression[i] === ")") {
19
+ tokens.push({ type: "rparen", value: ")", raw: ")" });
20
+ i++;
21
+ continue;
22
+ }
23
+ // Operators (multi-char first)
24
+ let matchedOp = false;
25
+ for (const op of OPERATORS) {
26
+ if (expression.startsWith(op, i)) {
27
+ tokens.push({ type: "operator", value: op, raw: op });
28
+ i += op.length;
29
+ matchedOp = true;
30
+ break;
31
+ }
32
+ }
33
+ if (matchedOp)
34
+ continue;
35
+ // NOT operator (! not followed by =)
36
+ if (expression[i] === "!" && expression[i + 1] !== "=") {
37
+ tokens.push({ type: "not", value: "!", raw: "!" });
38
+ i++;
39
+ continue;
40
+ }
41
+ // Variable: $varName
42
+ if (expression[i] === "$") {
43
+ const match = expression.slice(i).match(/^\$(\w+)/);
44
+ if (match) {
45
+ tokens.push({ type: "variable", value: match[1], raw: match[0] });
46
+ i += match[0].length;
47
+ continue;
48
+ }
49
+ }
50
+ // String literals (single or double quoted)
51
+ if (expression[i] === "'" || expression[i] === '"') {
52
+ const quote = expression[i];
53
+ let str = "";
54
+ i++; // skip opening quote
55
+ while (i < expression.length && expression[i] !== quote) {
56
+ if (expression[i] === "\\" && i + 1 < expression.length) {
57
+ str += expression[i + 1];
58
+ i += 2;
59
+ }
60
+ else {
61
+ str += expression[i];
62
+ i++;
63
+ }
64
+ }
65
+ if (i >= expression.length) {
66
+ throw new Error(`Unterminated string literal starting at position ${i - str.length - 1}`);
67
+ }
68
+ i++; // skip closing quote
69
+ tokens.push({ type: "string", value: str, raw: `${quote}${str}${quote}` });
70
+ continue;
71
+ }
72
+ // Number literals (including negative)
73
+ const numMatch = expression.slice(i).match(/^-?\d+(\.\d+)?/);
74
+ if (numMatch) {
75
+ // Make sure a negative number isn't just a minus operator
76
+ // A negative number is valid at start, after an operator, after lparen, or after 'not'
77
+ const prev = tokens.length > 0 ? tokens[tokens.length - 1] : null;
78
+ const isNegativeNum = expression[i] === "-"
79
+ ? (!prev || prev.type === "operator" || prev.type === "lparen" || prev.type === "not")
80
+ : true;
81
+ if (isNegativeNum) {
82
+ tokens.push({ type: "number", value: Number(numMatch[0]), raw: numMatch[0] });
83
+ i += numMatch[0].length;
84
+ continue;
85
+ }
86
+ }
87
+ // Boolean and null keywords
88
+ if (expression.startsWith("true", i) && !/\w/.test(expression[i + 4] || "")) {
89
+ tokens.push({ type: "boolean", value: true, raw: "true" });
90
+ i += 4;
91
+ continue;
92
+ }
93
+ if (expression.startsWith("false", i) && !/\w/.test(expression[i + 5] || "")) {
94
+ tokens.push({ type: "boolean", value: false, raw: "false" });
95
+ i += 5;
96
+ continue;
97
+ }
98
+ if (expression.startsWith("null", i) && !/\w/.test(expression[i + 4] || "")) {
99
+ tokens.push({ type: "null", value: null, raw: "null" });
100
+ i += 4;
101
+ continue;
102
+ }
103
+ // Unknown character — error
104
+ throw new Error(`Unexpected character '${expression[i]}' at position ${i}`);
105
+ }
106
+ tokens.push({ type: "eof", value: null, raw: "" });
107
+ return tokens;
108
+ }
109
+ // --- Recursive Descent Parser ---
110
+ class Parser {
111
+ tokens;
112
+ vars;
113
+ pos = 0;
114
+ constructor(tokens, vars) {
115
+ this.tokens = tokens;
116
+ this.vars = vars;
117
+ }
118
+ parse() {
119
+ const result = this.parseOr();
120
+ if (this.current().type !== "eof") {
121
+ throw new Error(`Unexpected token '${this.current().raw}' at end of expression`);
122
+ }
123
+ return result;
124
+ }
125
+ current() {
126
+ return this.tokens[this.pos];
127
+ }
128
+ advance() {
129
+ const token = this.tokens[this.pos];
130
+ this.pos++;
131
+ return token;
132
+ }
133
+ parseOr() {
134
+ let left = this.parseAnd();
135
+ while (this.current().type === "operator" && this.current().value === "||") {
136
+ this.advance();
137
+ const right = this.parseAnd();
138
+ left = left || right;
139
+ }
140
+ return left;
141
+ }
142
+ parseAnd() {
143
+ let left = this.parseComparison();
144
+ while (this.current().type === "operator" && this.current().value === "&&") {
145
+ this.advance();
146
+ const right = this.parseComparison();
147
+ left = left && right;
148
+ }
149
+ return left;
150
+ }
151
+ parseComparison() {
152
+ let left = this.parseUnary();
153
+ const compOps = ["===", "==", "!==", "!=", ">", "<", ">=", "<="];
154
+ while (this.current().type === "operator" && compOps.includes(this.current().value)) {
155
+ const op = this.advance().value;
156
+ const right = this.parseUnary();
157
+ left = this.applyComparison(left, op, right);
158
+ }
159
+ return left;
160
+ }
161
+ parseUnary() {
162
+ if (this.current().type === "not") {
163
+ this.advance();
164
+ const operand = this.parseUnary();
165
+ return !operand;
166
+ }
167
+ return this.parsePrimary();
168
+ }
169
+ parsePrimary() {
170
+ const token = this.current();
171
+ if (token.type === "lparen") {
172
+ this.advance(); // skip (
173
+ const value = this.parseOr();
174
+ if (this.current().type !== "rparen") {
175
+ throw new Error("Expected closing parenthesis");
176
+ }
177
+ this.advance(); // skip )
178
+ return value;
179
+ }
180
+ if (token.type === "variable") {
181
+ this.advance();
182
+ const varName = token.value;
183
+ return varName in this.vars ? this.vars[varName] : undefined;
184
+ }
185
+ if (token.type === "string") {
186
+ this.advance();
187
+ return token.value;
188
+ }
189
+ if (token.type === "number") {
190
+ this.advance();
191
+ return token.value;
192
+ }
193
+ if (token.type === "boolean") {
194
+ this.advance();
195
+ return token.value;
196
+ }
197
+ if (token.type === "null") {
198
+ this.advance();
199
+ return null;
200
+ }
201
+ throw new Error(`Unexpected token '${token.raw}'`);
202
+ }
203
+ applyComparison(left, op, right) {
204
+ switch (op) {
205
+ case "===": return left === right;
206
+ case "==": return left == right;
207
+ case "!==": return left !== right;
208
+ case "!=": return left != right;
209
+ case ">": return left > right;
210
+ case "<": return left < right;
211
+ case ">=": return left >= right;
212
+ case "<=": return left <= right;
213
+ default: return false;
214
+ }
215
+ }
216
+ }
217
+ /**
218
+ * Evaluate a simple condition expression against the vars map.
219
+ * Supports: ==, ===, !=, !==, >, <, >=, <=, &&, ||, !
220
+ * Variables via $varName syntax.
221
+ * String literals via single or double quotes.
222
+ * Number literals, boolean literals (true/false), null.
223
+ *
224
+ * SECURITY: No dynamic code evaluation. Parser-based.
225
+ * Unknown variables evaluate to undefined.
226
+ */
227
+ export function evaluateCondition(expression, vars) {
228
+ // Empty expression → true (step executes)
229
+ if (!expression || expression.trim() === "") {
230
+ return true;
231
+ }
232
+ try {
233
+ const tokens = tokenize(expression.trim());
234
+ const parser = new Parser(tokens, vars);
235
+ const result = parser.parse();
236
+ return !!result;
237
+ }
238
+ catch (err) {
239
+ console.error(`[plan-conditions] Invalid expression: "${expression}" — ${err instanceof Error ? err.message : String(err)}`);
240
+ return false;
241
+ }
242
+ }
@@ -0,0 +1,49 @@
1
+ import type { ToolRegistry } from "../registry.js";
2
+ import type { ToolResponse, ToolMeta } from "../types.js";
3
+ import type { VarsMap } from "./plan-variables.js";
4
+ import type { PlanStateStore } from "./plan-state-store.js";
5
+ export type ErrorStrategy = "abort" | "continue" | "screenshot";
6
+ export interface SuspendConfig {
7
+ /** Frage an den Agent */
8
+ question?: string;
9
+ /** Context-Typ: "screenshot" erzeugt automatisch einen Screenshot */
10
+ context?: "screenshot";
11
+ /** Bedingung: Plan pausiert NACH Step-Ausfuehrung wenn Bedingung true */
12
+ condition?: string;
13
+ }
14
+ export interface PlanStep {
15
+ tool: string;
16
+ params?: Record<string, unknown>;
17
+ saveAs?: string;
18
+ if?: string;
19
+ suspend?: SuspendConfig;
20
+ }
21
+ export interface StepResult {
22
+ step: number;
23
+ tool: string;
24
+ result: ToolResponse;
25
+ skipped?: boolean;
26
+ condition?: string;
27
+ }
28
+ export interface PlanOptions {
29
+ vars?: VarsMap;
30
+ errorStrategy?: ErrorStrategy;
31
+ /** Fuer Resume: der gespeicherte Plan-State */
32
+ resumeState?: {
33
+ suspendedAtIndex: number;
34
+ completedResults: StepResult[];
35
+ vars: VarsMap;
36
+ answer: string;
37
+ };
38
+ }
39
+ export interface SuspendedPlanResponse {
40
+ status: "suspended";
41
+ planId: string;
42
+ question: string;
43
+ completedSteps: StepResult[];
44
+ screenshot?: string;
45
+ _meta?: ToolMeta;
46
+ }
47
+ /** executePlan kann jetzt entweder ToolResponse oder SuspendedPlanResponse zurueckgeben */
48
+ export type PlanExecutionResult = ToolResponse | SuspendedPlanResponse;
49
+ export declare function executePlan(steps: PlanStep[], registry: ToolRegistry, options?: PlanOptions, stateStore?: PlanStateStore): Promise<PlanExecutionResult>;
@@ -0,0 +1,259 @@
1
+ import { substituteVars, extractResultValue } from "./plan-variables.js";
2
+ import { evaluateCondition } from "./plan-conditions.js";
3
+ const DEFAULT_SUSPEND_QUESTION = "Plan pausiert -- Bedingung erfuellt. Wie fortfahren?";
4
+ export async function executePlan(steps, registry, options, stateStore) {
5
+ const start = performance.now();
6
+ let results = [];
7
+ const vars = { ...(options?.vars ?? {}) };
8
+ const errorStrategy = options?.errorStrategy ?? "abort";
9
+ let startIndex = 0;
10
+ let isResumeFirstStep = false;
11
+ // --- Resume: restore state from previous suspend ---
12
+ if (options?.resumeState) {
13
+ const rs = options.resumeState;
14
+ Object.assign(vars, rs.vars);
15
+ vars["answer"] = rs.answer;
16
+ results = [...rs.completedResults];
17
+ startIndex = rs.suspendedAtIndex;
18
+ isResumeFirstStep = true;
19
+ }
20
+ for (let i = startIndex; i < steps.length; i++) {
21
+ const step = steps[i];
22
+ // --- Conditional: evaluate if clause ---
23
+ if (step.if !== undefined && step.if !== "") {
24
+ const conditionResult = evaluateCondition(step.if, vars);
25
+ if (!conditionResult) {
26
+ results.push({
27
+ step: i + 1,
28
+ tool: step.tool,
29
+ result: {
30
+ content: [{ type: "text", text: `Skipped: condition "${step.if}" was false` }],
31
+ _meta: { elapsedMs: 0, method: step.tool },
32
+ },
33
+ skipped: true,
34
+ condition: step.if,
35
+ });
36
+ continue;
37
+ }
38
+ }
39
+ // --- Pre-Suspend (without condition): pause BEFORE step execution ---
40
+ // Skip pre-suspend on the first step of a resume (agent already answered)
41
+ if (step.suspend && !step.suspend.condition && !isResumeFirstStep) {
42
+ if (!stateStore) {
43
+ console.warn("[plan-executor] suspend config on step but no stateStore provided — ignoring suspend");
44
+ }
45
+ else {
46
+ const question = step.suspend.question ?? DEFAULT_SUSPEND_QUESTION;
47
+ let screenshot;
48
+ if (step.suspend.context === "screenshot") {
49
+ try {
50
+ const ssResult = await registry.executeTool("screenshot", {});
51
+ if (!ssResult.isError) {
52
+ for (const block of ssResult.content) {
53
+ if (block.type === "image") {
54
+ screenshot = block.data;
55
+ break;
56
+ }
57
+ }
58
+ }
59
+ }
60
+ catch {
61
+ // Screenshot is best-effort
62
+ }
63
+ }
64
+ const planId = stateStore.suspend({
65
+ steps,
66
+ suspendedAtIndex: i,
67
+ vars: { ...vars },
68
+ errorStrategy,
69
+ completedResults: [...results],
70
+ question,
71
+ });
72
+ return {
73
+ status: "suspended",
74
+ planId,
75
+ question,
76
+ completedSteps: [...results],
77
+ screenshot,
78
+ _meta: {
79
+ elapsedMs: Math.round(performance.now() - start),
80
+ method: "run_plan",
81
+ },
82
+ };
83
+ }
84
+ }
85
+ // Reset resume-first-step flag after pre-suspend check
86
+ isResumeFirstStep = false;
87
+ // --- Variable substitution ---
88
+ const resolvedParams = step.params
89
+ ? substituteVars(step.params, vars)
90
+ : {};
91
+ // --- Execute step ---
92
+ let stepResult;
93
+ try {
94
+ stepResult = await registry.executeTool(step.tool, resolvedParams);
95
+ }
96
+ catch (err) {
97
+ const message = err instanceof Error ? err.message : String(err);
98
+ stepResult = {
99
+ content: [{ type: "text", text: `Exception in ${step.tool}: ${message}` }],
100
+ isError: true,
101
+ _meta: { elapsedMs: 0, method: step.tool },
102
+ };
103
+ }
104
+ // --- saveAs: store result as variable ---
105
+ if (!stepResult.isError && step.saveAs) {
106
+ vars[step.saveAs] = extractResultValue(stepResult);
107
+ }
108
+ results.push({ step: i + 1, tool: step.tool, result: stepResult });
109
+ // --- Post-Suspend (with condition): pause AFTER step execution if condition is true ---
110
+ if (step.suspend?.condition && !stepResult.isError) {
111
+ const suspendConditionResult = evaluateCondition(step.suspend.condition, vars);
112
+ if (suspendConditionResult) {
113
+ if (!stateStore) {
114
+ console.warn("[plan-executor] suspend condition met but no stateStore provided — ignoring suspend");
115
+ }
116
+ else {
117
+ const question = step.suspend.question ?? DEFAULT_SUSPEND_QUESTION;
118
+ let screenshot;
119
+ if (step.suspend.context === "screenshot") {
120
+ try {
121
+ const ssResult = await registry.executeTool("screenshot", {});
122
+ if (!ssResult.isError) {
123
+ for (const block of ssResult.content) {
124
+ if (block.type === "image") {
125
+ screenshot = block.data;
126
+ break;
127
+ }
128
+ }
129
+ }
130
+ }
131
+ catch {
132
+ // Screenshot is best-effort
133
+ }
134
+ }
135
+ const planId = stateStore.suspend({
136
+ steps,
137
+ suspendedAtIndex: i + 1,
138
+ vars: { ...vars },
139
+ errorStrategy,
140
+ completedResults: [...results],
141
+ question,
142
+ });
143
+ return {
144
+ status: "suspended",
145
+ planId,
146
+ question,
147
+ completedSteps: [...results],
148
+ screenshot,
149
+ _meta: {
150
+ elapsedMs: Math.round(performance.now() - start),
151
+ method: "run_plan",
152
+ },
153
+ };
154
+ }
155
+ }
156
+ }
157
+ // --- Error handling based on strategy ---
158
+ if (stepResult.isError) {
159
+ if (errorStrategy === "abort") {
160
+ return buildPlanResponse(results, steps.length, start, true, errorStrategy);
161
+ }
162
+ if (errorStrategy === "screenshot") {
163
+ // Take screenshot and append to the failed step
164
+ try {
165
+ const screenshotResult = await registry.executeTool("screenshot", {});
166
+ // Append screenshot content to the failed step's result
167
+ const lastResult = results[results.length - 1];
168
+ if (!screenshotResult.isError) {
169
+ for (const block of screenshotResult.content) {
170
+ if (block.type === "image") {
171
+ lastResult.result = {
172
+ ...lastResult.result,
173
+ content: [...lastResult.result.content, block],
174
+ };
175
+ }
176
+ }
177
+ }
178
+ }
179
+ catch {
180
+ // Screenshot is best-effort
181
+ }
182
+ return buildPlanResponse(results, steps.length, start, true, errorStrategy);
183
+ }
184
+ // errorStrategy === "continue": just keep going
185
+ }
186
+ }
187
+ return buildPlanResponse(results, steps.length, start, false, errorStrategy);
188
+ }
189
+ function buildPlanResponse(results, stepsTotal, startTime, aborted, errorStrategy = "abort") {
190
+ const elapsedMs = Math.round(performance.now() - startTime);
191
+ const contentBlocks = [];
192
+ let okCount = 0;
193
+ let failCount = 0;
194
+ let skipCount = 0;
195
+ for (const r of results) {
196
+ if (r.skipped) {
197
+ skipCount++;
198
+ contentBlocks.push({
199
+ type: "text",
200
+ text: `[${r.step}/${stepsTotal}] SKIP ${r.tool} (condition: ${r.condition})`,
201
+ });
202
+ continue;
203
+ }
204
+ const status = r.result.isError ? "FAIL" : "OK";
205
+ if (r.result.isError)
206
+ failCount++;
207
+ else
208
+ okCount++;
209
+ const stepMs = r.result._meta?.elapsedMs ?? 0;
210
+ // Build step header text from text content blocks
211
+ const textParts = r.result.content
212
+ .filter((c) => c.type === "text")
213
+ .map((c) => c.text)
214
+ .join("\n");
215
+ contentBlocks.push({
216
+ type: "text",
217
+ text: `[${r.step}/${stepsTotal}] ${status} ${r.tool} (${stepMs}ms): ${textParts}`,
218
+ });
219
+ // Preserve non-text content blocks (e.g. screenshot images)
220
+ for (const block of r.result.content) {
221
+ if (block.type !== "text") {
222
+ contentBlocks.push(block);
223
+ }
224
+ }
225
+ }
226
+ if (aborted) {
227
+ contentBlocks.push({
228
+ type: "text",
229
+ text: `\nPlan aborted at step ${results.length}/${stepsTotal}`,
230
+ });
231
+ }
232
+ // Summary for continue strategy with errors
233
+ if (errorStrategy === "continue" && failCount > 0 && !aborted) {
234
+ const parts = [`${okCount}/${stepsTotal} OK`, `${failCount} FAIL`];
235
+ if (skipCount > 0)
236
+ parts.push(`${skipCount} SKIP`);
237
+ contentBlocks.push({
238
+ type: "text",
239
+ text: `\nPlan completed with errors: ${parts.join(", ")}`,
240
+ });
241
+ }
242
+ // Determine isError:
243
+ // - abort/screenshot: aborted flag
244
+ // - continue: only if ALL executed (non-skipped) steps failed
245
+ const executedCount = okCount + failCount;
246
+ const isError = errorStrategy === "continue" && !aborted
247
+ ? executedCount > 0 && failCount === executedCount
248
+ : aborted;
249
+ return {
250
+ content: contentBlocks,
251
+ isError: isError || undefined,
252
+ _meta: {
253
+ elapsedMs,
254
+ method: "run_plan",
255
+ stepsTotal,
256
+ stepsCompleted: okCount,
257
+ },
258
+ };
259
+ }
@@ -0,0 +1,24 @@
1
+ import type { PlanStep, StepResult, ErrorStrategy } from "./plan-executor.js";
2
+ import type { VarsMap } from "./plan-variables.js";
3
+ export interface SuspendedPlanState {
4
+ planId: string;
5
+ steps: PlanStep[];
6
+ /** Index des Steps an dem der Plan pausiert wurde (naechster auszufuehrender Step) */
7
+ suspendedAtIndex: number;
8
+ vars: VarsMap;
9
+ errorStrategy: ErrorStrategy;
10
+ completedResults: StepResult[];
11
+ question: string;
12
+ createdAt: number;
13
+ }
14
+ export declare class PlanStateStore {
15
+ private store;
16
+ private readonly ttlMs;
17
+ constructor(ttlMs?: number);
18
+ /** Suspend speichert den Plan-State und gibt eine planId zurueck */
19
+ suspend(state: Omit<SuspendedPlanState, "planId" | "createdAt">): string;
20
+ /** Resume laedt und entfernt den Plan-State. Gibt null zurueck wenn abgelaufen oder nicht gefunden. */
21
+ resume(planId: string): SuspendedPlanState | null;
22
+ /** Bereinigt abgelaufene Eintraege (aufgerufen bei suspend) */
23
+ private cleanup;
24
+ }
@@ -0,0 +1,43 @@
1
+ import { randomUUID } from "node:crypto";
2
+ export class PlanStateStore {
3
+ store = new Map();
4
+ ttlMs;
5
+ constructor(ttlMs = 5 * 60 * 1000) {
6
+ this.ttlMs = ttlMs;
7
+ }
8
+ /** Suspend speichert den Plan-State und gibt eine planId zurueck */
9
+ suspend(state) {
10
+ this.cleanup();
11
+ const planId = randomUUID();
12
+ this.store.set(planId, {
13
+ ...state,
14
+ planId,
15
+ createdAt: Date.now(),
16
+ });
17
+ return planId;
18
+ }
19
+ /** Resume laedt und entfernt den Plan-State. Gibt null zurueck wenn abgelaufen oder nicht gefunden. */
20
+ resume(planId) {
21
+ this.cleanup();
22
+ const state = this.store.get(planId);
23
+ if (!state) {
24
+ return null;
25
+ }
26
+ // TTL check (>= so that TTL=0 expires immediately)
27
+ if (Date.now() - state.createdAt >= this.ttlMs) {
28
+ this.store.delete(planId);
29
+ return null;
30
+ }
31
+ this.store.delete(planId);
32
+ return state;
33
+ }
34
+ /** Bereinigt abgelaufene Eintraege (aufgerufen bei suspend) */
35
+ cleanup() {
36
+ const now = Date.now();
37
+ for (const [id, state] of this.store) {
38
+ if (now - state.createdAt >= this.ttlMs) {
39
+ this.store.delete(id);
40
+ }
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,16 @@
1
+ import type { ToolResponse } from "../types.js";
2
+ export type VarsMap = Record<string, unknown>;
3
+ /**
4
+ * Substitute $var references in a params object.
5
+ * Replaces string values matching "$varName" pattern with the value from vars.
6
+ * Supports nested objects and arrays.
7
+ * Unresolved $var references remain as-is (no error — the tool will handle invalid params).
8
+ */
9
+ export declare function substituteVars(params: Record<string, unknown>, vars: VarsMap): Record<string, unknown>;
10
+ /**
11
+ * Extract the text content from a ToolResponse for saveAs.
12
+ * Concatenates all text content blocks into a single string.
13
+ * If the text is valid JSON, parse it and return the parsed value.
14
+ * Otherwise return the raw text string.
15
+ */
16
+ export declare function extractResultValue(result: ToolResponse): unknown;