outcome-cli 1.0.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 (113) hide show
  1. package/README.md +261 -0
  2. package/package.json +95 -0
  3. package/src/agents/README.md +139 -0
  4. package/src/agents/adapters/anthropic.adapter.ts +166 -0
  5. package/src/agents/adapters/dalle.adapter.ts +145 -0
  6. package/src/agents/adapters/gemini.adapter.ts +134 -0
  7. package/src/agents/adapters/imagen.adapter.ts +106 -0
  8. package/src/agents/adapters/nano-banana.adapter.ts +129 -0
  9. package/src/agents/adapters/openai.adapter.ts +165 -0
  10. package/src/agents/adapters/veo.adapter.ts +130 -0
  11. package/src/agents/agent.schema.property.test.ts +379 -0
  12. package/src/agents/agent.schema.test.ts +148 -0
  13. package/src/agents/agent.schema.ts +263 -0
  14. package/src/agents/index.ts +60 -0
  15. package/src/agents/registered-agent.schema.ts +356 -0
  16. package/src/agents/registry.ts +97 -0
  17. package/src/agents/tournament-configs.property.test.ts +266 -0
  18. package/src/cli/README.md +145 -0
  19. package/src/cli/commands/define.ts +79 -0
  20. package/src/cli/commands/list.ts +46 -0
  21. package/src/cli/commands/logs.ts +83 -0
  22. package/src/cli/commands/run.ts +416 -0
  23. package/src/cli/commands/verify.ts +110 -0
  24. package/src/cli/index.ts +81 -0
  25. package/src/config/README.md +128 -0
  26. package/src/config/env.ts +262 -0
  27. package/src/config/index.ts +19 -0
  28. package/src/eval/README.md +318 -0
  29. package/src/eval/ai-judge.test.ts +435 -0
  30. package/src/eval/ai-judge.ts +368 -0
  31. package/src/eval/code-validators.ts +414 -0
  32. package/src/eval/evaluateOutcome.property.test.ts +1174 -0
  33. package/src/eval/evaluateOutcome.ts +591 -0
  34. package/src/eval/immigration-validators.ts +122 -0
  35. package/src/eval/index.ts +90 -0
  36. package/src/eval/judge-cache.ts +402 -0
  37. package/src/eval/tournament-validators.property.test.ts +439 -0
  38. package/src/eval/validators.property.test.ts +1118 -0
  39. package/src/eval/validators.ts +1199 -0
  40. package/src/eval/weighted-scorer.ts +285 -0
  41. package/src/index.ts +17 -0
  42. package/src/league/README.md +188 -0
  43. package/src/league/health-check.ts +353 -0
  44. package/src/league/index.ts +93 -0
  45. package/src/league/killAgent.ts +151 -0
  46. package/src/league/league.test.ts +1151 -0
  47. package/src/league/runLeague.ts +843 -0
  48. package/src/league/scoreAgent.ts +175 -0
  49. package/src/modules/omnibridge/__tests__/.gitkeep +1 -0
  50. package/src/modules/omnibridge/__tests__/auth-tunnel.property.test.ts +524 -0
  51. package/src/modules/omnibridge/__tests__/deterministic-logger.property.test.ts +965 -0
  52. package/src/modules/omnibridge/__tests__/ghost-api.property.test.ts +461 -0
  53. package/src/modules/omnibridge/__tests__/omnibridge-integration.test.ts +542 -0
  54. package/src/modules/omnibridge/__tests__/parallel-executor.property.test.ts +671 -0
  55. package/src/modules/omnibridge/__tests__/semantic-normalizer.property.test.ts +521 -0
  56. package/src/modules/omnibridge/__tests__/semantic-normalizer.test.ts +254 -0
  57. package/src/modules/omnibridge/__tests__/session-vault.property.test.ts +367 -0
  58. package/src/modules/omnibridge/__tests__/shadow-session.property.test.ts +523 -0
  59. package/src/modules/omnibridge/__tests__/triangulation-engine.property.test.ts +292 -0
  60. package/src/modules/omnibridge/__tests__/verification-engine.property.test.ts +769 -0
  61. package/src/modules/omnibridge/api/.gitkeep +1 -0
  62. package/src/modules/omnibridge/api/ghost-api.ts +1087 -0
  63. package/src/modules/omnibridge/auth/.gitkeep +1 -0
  64. package/src/modules/omnibridge/auth/auth-tunnel.ts +843 -0
  65. package/src/modules/omnibridge/auth/session-vault.ts +577 -0
  66. package/src/modules/omnibridge/core/.gitkeep +1 -0
  67. package/src/modules/omnibridge/core/semantic-normalizer.ts +702 -0
  68. package/src/modules/omnibridge/core/triangulation-engine.ts +530 -0
  69. package/src/modules/omnibridge/core/types.ts +610 -0
  70. package/src/modules/omnibridge/execution/.gitkeep +1 -0
  71. package/src/modules/omnibridge/execution/deterministic-logger.ts +629 -0
  72. package/src/modules/omnibridge/execution/parallel-executor.ts +542 -0
  73. package/src/modules/omnibridge/execution/shadow-session.ts +794 -0
  74. package/src/modules/omnibridge/index.ts +212 -0
  75. package/src/modules/omnibridge/omnibridge.ts +510 -0
  76. package/src/modules/omnibridge/verification/.gitkeep +1 -0
  77. package/src/modules/omnibridge/verification/verification-engine.ts +783 -0
  78. package/src/outcomes/README.md +75 -0
  79. package/src/outcomes/acquire-pilot-customer.ts +297 -0
  80. package/src/outcomes/code-delivery-outcomes.ts +89 -0
  81. package/src/outcomes/code-outcomes.ts +256 -0
  82. package/src/outcomes/code_review_battle.test.ts +135 -0
  83. package/src/outcomes/code_review_battle.ts +135 -0
  84. package/src/outcomes/cold_email_battle.ts +97 -0
  85. package/src/outcomes/content_creation_battle.ts +160 -0
  86. package/src/outcomes/f1_stem_opt_compliance.ts +61 -0
  87. package/src/outcomes/index.ts +107 -0
  88. package/src/outcomes/lead_gen_battle.test.ts +113 -0
  89. package/src/outcomes/lead_gen_battle.ts +99 -0
  90. package/src/outcomes/outcome.schema.property.test.ts +229 -0
  91. package/src/outcomes/outcome.schema.ts +187 -0
  92. package/src/outcomes/qualified_sales_interest.ts +118 -0
  93. package/src/outcomes/swarm_planner.property.test.ts +370 -0
  94. package/src/outcomes/swarm_planner.ts +96 -0
  95. package/src/outcomes/web_extraction.ts +234 -0
  96. package/src/runtime/README.md +220 -0
  97. package/src/runtime/agentRunner.test.ts +341 -0
  98. package/src/runtime/agentRunner.ts +746 -0
  99. package/src/runtime/claudeAdapter.ts +232 -0
  100. package/src/runtime/costTracker.ts +123 -0
  101. package/src/runtime/index.ts +34 -0
  102. package/src/runtime/modelAdapter.property.test.ts +305 -0
  103. package/src/runtime/modelAdapter.ts +144 -0
  104. package/src/runtime/openaiAdapter.ts +235 -0
  105. package/src/utils/README.md +122 -0
  106. package/src/utils/command-runner.ts +134 -0
  107. package/src/utils/cost-guard.ts +379 -0
  108. package/src/utils/errors.test.ts +290 -0
  109. package/src/utils/errors.ts +442 -0
  110. package/src/utils/index.ts +37 -0
  111. package/src/utils/logger.test.ts +361 -0
  112. package/src/utils/logger.ts +419 -0
  113. package/src/utils/output-parsers.ts +216 -0
@@ -0,0 +1,1087 @@
1
+ /**
2
+ * Ghost-API
3
+ *
4
+ * Clean JSON interface that agents use to interact with websites.
5
+ * Agents never see HTML - they see structured endpoints and responses.
6
+ *
7
+ * Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6
8
+ */
9
+
10
+ import { createHash } from 'node:crypto';
11
+ import type {
12
+ GoalDefinition,
13
+ SchemaMapping,
14
+ GhostResponse,
15
+ IntentDocument,
16
+ IntentElement,
17
+ ActionLogEntry,
18
+ } from '../core/types.js';
19
+ import type { SemanticNormalizer } from '../core/semantic-normalizer.js';
20
+ import type { TriangulationEngine } from '../core/triangulation-engine.js';
21
+ import type { DeterministicLogger } from '../execution/deterministic-logger.js';
22
+ import type { ShadowSessionOrchestrator } from '../execution/shadow-session.js';
23
+
24
+ // =============================================================================
25
+ // Types
26
+ // =============================================================================
27
+
28
+ /**
29
+ * Configuration for the Ghost-API.
30
+ */
31
+ export interface GhostApiConfig {
32
+ /** Semantic Normalizer for DOM → Intent conversion */
33
+ normalizer: SemanticNormalizer;
34
+ /** Triangulation Engine for self-healing element location */
35
+ triangulationEngine: TriangulationEngine;
36
+ /** Deterministic Logger for action logging */
37
+ logger: DeterministicLogger;
38
+ /** Shadow Session Orchestrator for browser sessions */
39
+ sessionOrchestrator?: ShadowSessionOrchestrator;
40
+ /** Base path for generated endpoints (default: '/omni-bridge') */
41
+ basePath?: string;
42
+ /** Maximum pages to handle for pagination (default: 10) */
43
+ maxPaginationPages?: number;
44
+ /** Timeout for goal execution in milliseconds (default: 30000) */
45
+ executionTimeoutMs?: number;
46
+ }
47
+
48
+ /**
49
+ * Execution context for a Ghost-API call.
50
+ */
51
+ export interface ExecutionContext {
52
+ /** Session ID for the execution */
53
+ sessionId: string;
54
+ /** Goal being executed */
55
+ goal: GoalDefinition;
56
+ /** Schema mapping for the goal */
57
+ schemaMapping: SchemaMapping;
58
+ /** Start time of execution */
59
+ startTime: number;
60
+ /** Actions performed count */
61
+ actionsPerformed: number;
62
+ /** Triangulation heals count */
63
+ triangulationHeals: number;
64
+ /** Extracted data */
65
+ extractedData: Record<string, unknown>[];
66
+ /** Current page number (for pagination) */
67
+ currentPage: number;
68
+ }
69
+
70
+ /**
71
+ * Result of a DOM action.
72
+ */
73
+ export interface ActionResult {
74
+ /** Whether the action succeeded */
75
+ success: boolean;
76
+ /** Error message if failed */
77
+ error?: string;
78
+ /** Extracted data if applicable */
79
+ data?: Record<string, unknown>;
80
+ }
81
+
82
+ /**
83
+ * Pagination detection result.
84
+ */
85
+ export interface PaginationInfo {
86
+ /** Whether pagination is detected */
87
+ hasPagination: boolean;
88
+ /** Next page element if found */
89
+ nextPageElement?: IntentElement;
90
+ /** Total pages if detectable */
91
+ totalPages?: number;
92
+ /** Current page number */
93
+ currentPage: number;
94
+ }
95
+
96
+ /**
97
+ * Goal execution result.
98
+ */
99
+ export interface GoalExecutionResult {
100
+ /** Whether execution succeeded */
101
+ success: boolean;
102
+ /** Ghost response if successful */
103
+ response?: GhostResponse;
104
+ /** Error message if failed */
105
+ error?: string;
106
+ }
107
+
108
+ // =============================================================================
109
+ // Schema Inference
110
+ // =============================================================================
111
+
112
+ /**
113
+ * Infer a JSON schema from an Intent Document.
114
+ * Analyzes the document structure to generate a schema for the goal.
115
+ */
116
+ export function inferSchemaFromDocument(
117
+ document: IntentDocument,
118
+ goal: GoalDefinition
119
+ ): Record<string, unknown> {
120
+ const schema: Record<string, unknown> = {
121
+ type: 'object',
122
+ properties: {},
123
+ required: [],
124
+ };
125
+
126
+ const properties = schema.properties as Record<string, unknown>;
127
+ const required = schema.required as string[];
128
+
129
+ // Analyze forms for input/output fields
130
+ for (const form of document.forms) {
131
+ for (const field of form.fields) {
132
+ const fieldName = sanitizeFieldName(field.name || field.label);
133
+ if (fieldName) {
134
+ properties[fieldName] = inferFieldSchema(field.type, field.label);
135
+ if (field.required) {
136
+ required.push(fieldName);
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ // Analyze display elements for data extraction
143
+ const displayElements = document.elements.filter(el => el.role === 'display');
144
+ for (const element of displayElements) {
145
+ const fieldName = sanitizeFieldName(element.label);
146
+ if (fieldName && !properties[fieldName]) {
147
+ properties[fieldName] = {
148
+ type: 'string',
149
+ description: `Extracted from ${element.intentId}`,
150
+ };
151
+ }
152
+ }
153
+
154
+ // If goal has output schema, merge it
155
+ if (goal.outputSchema) {
156
+ return mergeSchemas(schema, goal.outputSchema);
157
+ }
158
+
159
+ return schema;
160
+ }
161
+
162
+ /**
163
+ * Sanitize a field name for use in JSON schema.
164
+ */
165
+ function sanitizeFieldName(name: string): string {
166
+ if (!name) return '';
167
+ return name
168
+ .toLowerCase()
169
+ .replace(/[^a-z0-9]+/g, '_')
170
+ .replace(/^_+|_+$/g, '')
171
+ .slice(0, 50);
172
+ }
173
+
174
+ /**
175
+ * Infer field schema from input type.
176
+ */
177
+ function inferFieldSchema(type: string, label: string): Record<string, unknown> {
178
+ const labelLower = label.toLowerCase();
179
+
180
+ switch (type) {
181
+ case 'email':
182
+ return { type: 'string', format: 'email' };
183
+ case 'number':
184
+ case 'range':
185
+ return { type: 'number' };
186
+ case 'checkbox':
187
+ return { type: 'boolean' };
188
+ case 'date':
189
+ return { type: 'string', format: 'date' };
190
+ case 'datetime-local':
191
+ return { type: 'string', format: 'date-time' };
192
+ case 'url':
193
+ return { type: 'string', format: 'uri' };
194
+ case 'tel':
195
+ return { type: 'string', pattern: '^[+]?[0-9\\s-]+$' };
196
+ default:
197
+ // Infer from label
198
+ if (labelLower.includes('email')) {
199
+ return { type: 'string', format: 'email' };
200
+ }
201
+ if (labelLower.includes('phone') || labelLower.includes('tel')) {
202
+ return { type: 'string', pattern: '^[+]?[0-9\\s-]+$' };
203
+ }
204
+ if (labelLower.includes('date')) {
205
+ return { type: 'string', format: 'date' };
206
+ }
207
+ if (labelLower.includes('url') || labelLower.includes('website')) {
208
+ return { type: 'string', format: 'uri' };
209
+ }
210
+ if (labelLower.includes('amount') || labelLower.includes('price') || labelLower.includes('total')) {
211
+ return { type: 'number' };
212
+ }
213
+ return { type: 'string' };
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Merge two JSON schemas.
219
+ */
220
+ function mergeSchemas(
221
+ base: Record<string, unknown>,
222
+ override: Record<string, unknown>
223
+ ): Record<string, unknown> {
224
+ const merged = { ...base };
225
+
226
+ if (override.properties && typeof override.properties === 'object') {
227
+ merged.properties = {
228
+ ...(base.properties as Record<string, unknown> || {}),
229
+ ...(override.properties as Record<string, unknown>),
230
+ };
231
+ }
232
+
233
+ if (override.required && Array.isArray(override.required)) {
234
+ const baseRequired = (base.required as string[]) || [];
235
+ merged.required = [...new Set([...baseRequired, ...override.required])];
236
+ }
237
+
238
+ return merged;
239
+ }
240
+
241
+ // =============================================================================
242
+ // Endpoint Generation
243
+ // =============================================================================
244
+
245
+ /**
246
+ * Generate a clean endpoint path from a goal definition.
247
+ */
248
+ export function generateEndpointPath(goal: GoalDefinition, basePath: string): string {
249
+ // Extract domain from target URL
250
+ let domain = 'unknown';
251
+ try {
252
+ const url = new URL(goal.targetUrl);
253
+ domain = url.hostname.replace(/^www\./, '').split('.')[0];
254
+ } catch {
255
+ // Use goal name if URL parsing fails
256
+ domain = goal.name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
257
+ }
258
+
259
+ // Convert goal name to kebab-case
260
+ const goalPath = goal.name
261
+ .toLowerCase()
262
+ .replace(/[^a-z0-9]+/g, '-')
263
+ .replace(/^-+|-+$/g, '');
264
+
265
+ return `${basePath}/${domain}/${goalPath}`;
266
+ }
267
+
268
+ /**
269
+ * Estimate the number of actions needed to complete a goal.
270
+ */
271
+ export function estimateActions(document: IntentDocument, goal: GoalDefinition): number {
272
+ let estimate = 1; // At least one action (navigation)
273
+
274
+ // Add actions for forms
275
+ for (const form of document.forms) {
276
+ estimate += form.fields.length; // One action per field
277
+ estimate += 1; // Submit action
278
+ }
279
+
280
+ // Add actions for navigation
281
+ estimate += document.navigation.primaryLinks.length > 0 ? 1 : 0;
282
+
283
+ // Add actions for data extraction
284
+ const displayElements = document.elements.filter(el => el.role === 'display');
285
+ estimate += Math.ceil(displayElements.length / 10); // Batch extraction
286
+
287
+ // Factor in goal complexity from description
288
+ const descLower = goal.description.toLowerCase();
289
+ if (descLower.includes('multiple') || descLower.includes('all')) {
290
+ estimate *= 2;
291
+ }
292
+ if (descLower.includes('pagination') || descLower.includes('pages')) {
293
+ estimate *= 3;
294
+ }
295
+
296
+ return Math.min(estimate, 100); // Cap at 100
297
+ }
298
+
299
+ // =============================================================================
300
+ // Ghost-API Implementation
301
+ // =============================================================================
302
+
303
+ /**
304
+ * Ghost-API class
305
+ *
306
+ * Provides a clean JSON interface for agents to interact with websites.
307
+ * Handles all DOM interaction transparently.
308
+ *
309
+ * Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6
310
+ */
311
+ export class GhostApi {
312
+ private readonly normalizer: SemanticNormalizer;
313
+ // @ts-expect-error Reserved for future self-healing element location implementation
314
+ private readonly _triangulationEngine: TriangulationEngine;
315
+ private readonly logger: DeterministicLogger;
316
+ // @ts-expect-error Reserved for future browser session management implementation
317
+ private readonly _sessionOrchestrator?: ShadowSessionOrchestrator;
318
+ private readonly basePath: string;
319
+ private readonly maxPaginationPages: number;
320
+ private readonly executionTimeoutMs: number;
321
+
322
+ /** Registered goals and their schema mappings */
323
+ private goals: Map<string, { goal: GoalDefinition; mapping: SchemaMapping }> = new Map();
324
+
325
+ /** Active execution contexts */
326
+ private contexts: Map<string, ExecutionContext> = new Map();
327
+
328
+ constructor(config: GhostApiConfig) {
329
+ this.normalizer = config.normalizer;
330
+ this._triangulationEngine = config.triangulationEngine;
331
+ this.logger = config.logger;
332
+ this._sessionOrchestrator = config.sessionOrchestrator;
333
+ this.basePath = config.basePath ?? '/omni-bridge';
334
+ this.maxPaginationPages = config.maxPaginationPages ?? 10;
335
+ this.executionTimeoutMs = config.executionTimeoutMs ?? 30000;
336
+ }
337
+
338
+ // ===========================================================================
339
+ // Goal Definition and Schema Mapping (Requirements 3.1, 3.2)
340
+ // ===========================================================================
341
+
342
+ /**
343
+ * Define a goal and generate a schema mapping.
344
+ *
345
+ * Requirements: 3.1, 3.2
346
+ */
347
+ async defineGoal(goal: GoalDefinition, html?: string): Promise<SchemaMapping> {
348
+ // If HTML is provided, use it to infer schema
349
+ let document: IntentDocument | undefined;
350
+ if (html) {
351
+ document = this.normalizer.normalize(html, goal.targetUrl);
352
+ }
353
+
354
+ // Generate endpoint path
355
+ const endpoint = generateEndpointPath(goal, this.basePath);
356
+
357
+ // Infer or use provided schema
358
+ let schema: Record<string, unknown>;
359
+ if (goal.outputSchema) {
360
+ schema = goal.outputSchema;
361
+ } else if (document) {
362
+ schema = inferSchemaFromDocument(document, goal);
363
+ } else {
364
+ // Default schema for unknown structure
365
+ schema = {
366
+ type: 'object',
367
+ properties: {
368
+ data: { type: 'array', items: { type: 'object' } },
369
+ },
370
+ };
371
+ }
372
+
373
+ // Determine if auth is required (heuristic based on URL and goal)
374
+ const requiresAuth = this.detectAuthRequirement(goal);
375
+
376
+ // Estimate actions
377
+ const estimatedActions = document
378
+ ? estimateActions(document, goal)
379
+ : 10; // Default estimate
380
+
381
+ const mapping: SchemaMapping = {
382
+ endpoint,
383
+ schema,
384
+ requiredAuth: requiresAuth,
385
+ estimatedActions,
386
+ };
387
+
388
+ // Store the goal and mapping
389
+ this.goals.set(endpoint, { goal, mapping });
390
+
391
+ return mapping;
392
+ }
393
+
394
+ /**
395
+ * Detect if authentication is likely required for a goal.
396
+ */
397
+ private detectAuthRequirement(goal: GoalDefinition): boolean {
398
+ const urlLower = goal.targetUrl.toLowerCase();
399
+ const descLower = goal.description.toLowerCase();
400
+ const nameLower = goal.name.toLowerCase();
401
+
402
+ const authKeywords = [
403
+ 'login', 'signin', 'sign-in', 'auth', 'account', 'dashboard',
404
+ 'portal', 'admin', 'private', 'secure', 'member', 'profile',
405
+ 'invoice', 'billing', 'payment', 'order', 'subscription',
406
+ ];
407
+
408
+ return authKeywords.some(keyword =>
409
+ urlLower.includes(keyword) ||
410
+ descLower.includes(keyword) ||
411
+ nameLower.includes(keyword)
412
+ );
413
+ }
414
+
415
+ /**
416
+ * Get the schema for a target URL.
417
+ *
418
+ * Requirements: 3.2
419
+ */
420
+ async getSchema(targetUrl: string, html?: string): Promise<Record<string, unknown>> {
421
+ // Create a temporary goal for schema inference
422
+ const tempGoal: GoalDefinition = {
423
+ name: 'Schema_Discovery',
424
+ targetUrl,
425
+ description: 'Discover schema for target URL',
426
+ };
427
+
428
+ if (html) {
429
+ const document = this.normalizer.normalize(html, targetUrl);
430
+ return inferSchemaFromDocument(document, tempGoal);
431
+ }
432
+
433
+ // Return default schema if no HTML provided
434
+ return {
435
+ type: 'object',
436
+ properties: {
437
+ data: { type: 'array', items: { type: 'object' } },
438
+ },
439
+ };
440
+ }
441
+
442
+ /**
443
+ * Get a registered goal by endpoint.
444
+ */
445
+ getGoal(endpoint: string): { goal: GoalDefinition; mapping: SchemaMapping } | undefined {
446
+ return this.goals.get(endpoint);
447
+ }
448
+
449
+ /**
450
+ * List all registered endpoints.
451
+ */
452
+ listEndpoints(): string[] {
453
+ return Array.from(this.goals.keys());
454
+ }
455
+
456
+
457
+ // ===========================================================================
458
+ // Transparent DOM Interaction (Requirement 3.3)
459
+ // ===========================================================================
460
+
461
+ /**
462
+ * Execute a goal and return structured JSON.
463
+ * Handles all DOM interaction transparently - agent never sees HTML.
464
+ *
465
+ * Requirements: 3.3, 3.4, 3.5, 3.6
466
+ */
467
+ async execute(
468
+ endpoint: string,
469
+ params?: Record<string, unknown>,
470
+ html?: string
471
+ ): Promise<GhostResponse> {
472
+ const startTime = performance.now();
473
+ const registered = this.goals.get(endpoint);
474
+
475
+ if (!registered) {
476
+ throw new Error(`Endpoint not found: ${endpoint}`);
477
+ }
478
+
479
+ const { goal, mapping } = registered;
480
+
481
+ // Create execution context
482
+ const sessionId = `ghost_${Date.now()}_${Math.random().toString(36).slice(2)}`;
483
+ const context: ExecutionContext = {
484
+ sessionId,
485
+ goal,
486
+ schemaMapping: mapping,
487
+ startTime,
488
+ actionsPerformed: 0,
489
+ triangulationHeals: 0,
490
+ extractedData: [],
491
+ currentPage: 1,
492
+ };
493
+ this.contexts.set(sessionId, context);
494
+
495
+ try {
496
+ // If HTML is provided, process it directly
497
+ if (html) {
498
+ return await this.processHtml(context, html, params);
499
+ }
500
+
501
+ // Otherwise, return a placeholder response (real implementation would use browser)
502
+ return this.createResponse(context, []);
503
+ } finally {
504
+ this.contexts.delete(sessionId);
505
+ }
506
+ }
507
+
508
+ /**
509
+ * Process HTML and extract data according to the goal.
510
+ */
511
+ private async processHtml(
512
+ context: ExecutionContext,
513
+ html: string,
514
+ params?: Record<string, unknown>
515
+ ): Promise<GhostResponse> {
516
+ // Normalize the HTML to Intent Document
517
+ const document = this.normalizer.normalize(html, context.goal.targetUrl);
518
+
519
+ // Log the navigation action
520
+ this.logAction(context, 'navigate', 'NAV_ID:PAGE_LOAD', 'success');
521
+
522
+ // Handle form filling if params provided
523
+ if (params && Object.keys(params).length > 0) {
524
+ await this.handleFormFilling(context, document, params);
525
+ }
526
+
527
+ // Extract data from the document
528
+ const extractedData = this.extractData(context, document);
529
+ context.extractedData.push(...extractedData);
530
+
531
+ // Check for pagination
532
+ const paginationInfo = this.detectPagination(document);
533
+ if (paginationInfo.hasPagination && context.currentPage < this.maxPaginationPages) {
534
+ // In a real implementation, we would navigate to next page
535
+ // For now, we just note that pagination exists
536
+ this.logAction(context, 'extract', 'NAV_ID:PAGINATION_DETECTED', 'success');
537
+ }
538
+
539
+ return this.createResponse(context, context.extractedData);
540
+ }
541
+
542
+ /**
543
+ * Handle form filling with provided parameters.
544
+ */
545
+ private async handleFormFilling(
546
+ context: ExecutionContext,
547
+ document: IntentDocument,
548
+ params: Record<string, unknown>
549
+ ): Promise<void> {
550
+ for (const form of document.forms) {
551
+ for (const field of form.fields) {
552
+ const fieldName = sanitizeFieldName(field.name || field.label);
553
+ if (fieldName && params[fieldName] !== undefined) {
554
+ // Log the typing action
555
+ this.logAction(
556
+ context,
557
+ 'type',
558
+ field.intentId,
559
+ 'success',
560
+ String(params[fieldName])
561
+ );
562
+ }
563
+ }
564
+
565
+ // Log form submission if we filled any fields
566
+ if (form.submitButtonId) {
567
+ this.logAction(context, 'click', form.submitButtonId, 'success');
568
+ }
569
+ }
570
+ }
571
+
572
+ /**
573
+ * Extract data from an Intent Document.
574
+ */
575
+ private extractData(
576
+ context: ExecutionContext,
577
+ document: IntentDocument
578
+ ): Record<string, unknown>[] {
579
+ const results: Record<string, unknown>[] = [];
580
+ const dataItem: Record<string, unknown> = {};
581
+
582
+ // Extract from display elements
583
+ for (const element of document.elements) {
584
+ if (element.role === 'display' && element.label) {
585
+ const fieldName = sanitizeFieldName(element.label);
586
+ if (fieldName) {
587
+ dataItem[fieldName] = element.label;
588
+ this.logAction(context, 'extract', element.intentId, 'success');
589
+ }
590
+ }
591
+ }
592
+
593
+ // Extract from forms (read-only data)
594
+ for (const form of document.forms) {
595
+ for (const field of form.fields) {
596
+ const fieldName = sanitizeFieldName(field.name || field.label);
597
+ if (fieldName && field.placeholder) {
598
+ dataItem[fieldName] = field.placeholder;
599
+ }
600
+ }
601
+ }
602
+
603
+ if (Object.keys(dataItem).length > 0) {
604
+ results.push(dataItem);
605
+ }
606
+
607
+ return results;
608
+ }
609
+
610
+ /**
611
+ * Log an action to the deterministic logger.
612
+ */
613
+ private logAction(
614
+ context: ExecutionContext,
615
+ action: ActionLogEntry['action'],
616
+ intentId: string,
617
+ result: ActionLogEntry['result'],
618
+ value?: string
619
+ ): void {
620
+ context.actionsPerformed++;
621
+
622
+ this.logger.logAction({
623
+ sessionId: context.sessionId,
624
+ action,
625
+ intentId,
626
+ result,
627
+ value,
628
+ });
629
+ }
630
+
631
+ // ===========================================================================
632
+ // Transparent DOM Interaction Helpers (Requirement 3.3)
633
+ // ===========================================================================
634
+
635
+ /**
636
+ * Simulate clicking an element.
637
+ * Agent never sees the actual DOM - just the result.
638
+ *
639
+ * Requirement: 3.3
640
+ */
641
+ async click(
642
+ context: ExecutionContext,
643
+ element: IntentElement
644
+ ): Promise<ActionResult> {
645
+ try {
646
+ // Log the click action
647
+ this.logAction(context, 'click', element.intentId, 'success');
648
+
649
+ return {
650
+ success: true,
651
+ };
652
+ } catch (error) {
653
+ this.logAction(context, 'click', element.intentId, 'failure');
654
+ return {
655
+ success: false,
656
+ error: error instanceof Error ? error.message : 'Click failed',
657
+ };
658
+ }
659
+ }
660
+
661
+ /**
662
+ * Simulate typing into an element.
663
+ * Agent never sees the actual DOM - just the result.
664
+ *
665
+ * Requirement: 3.3
666
+ */
667
+ async type(
668
+ context: ExecutionContext,
669
+ element: IntentElement,
670
+ value: string
671
+ ): Promise<ActionResult> {
672
+ try {
673
+ // Log the type action
674
+ this.logAction(context, 'type', element.intentId, 'success', value);
675
+
676
+ return {
677
+ success: true,
678
+ };
679
+ } catch (error) {
680
+ this.logAction(context, 'type', element.intentId, 'failure', value);
681
+ return {
682
+ success: false,
683
+ error: error instanceof Error ? error.message : 'Type failed',
684
+ };
685
+ }
686
+ }
687
+
688
+ /**
689
+ * Simulate navigation to a URL.
690
+ * Agent never sees the actual DOM - just the result.
691
+ *
692
+ * Requirement: 3.3
693
+ */
694
+ async navigate(
695
+ context: ExecutionContext,
696
+ url: string
697
+ ): Promise<ActionResult> {
698
+ try {
699
+ // Log the navigation action
700
+ this.logAction(context, 'navigate', `NAV_ID:${url}`, 'success', url);
701
+
702
+ return {
703
+ success: true,
704
+ };
705
+ } catch (error) {
706
+ this.logAction(context, 'navigate', `NAV_ID:${url}`, 'failure', url);
707
+ return {
708
+ success: false,
709
+ error: error instanceof Error ? error.message : 'Navigation failed',
710
+ };
711
+ }
712
+ }
713
+
714
+ /**
715
+ * Wait for a condition or timeout.
716
+ *
717
+ * Requirement: 3.3
718
+ */
719
+ async wait(
720
+ context: ExecutionContext,
721
+ condition: string,
722
+ timeoutMs: number = 5000
723
+ ): Promise<ActionResult> {
724
+ try {
725
+ // Log the wait action
726
+ this.logAction(context, 'wait', `WAIT:${condition}`, 'success', String(timeoutMs));
727
+
728
+ return {
729
+ success: true,
730
+ };
731
+ } catch (error) {
732
+ this.logAction(context, 'wait', `WAIT:${condition}`, 'failure');
733
+ return {
734
+ success: false,
735
+ error: error instanceof Error ? error.message : 'Wait failed',
736
+ };
737
+ }
738
+ }
739
+
740
+ /**
741
+ * Extract data from an element.
742
+ * Agent receives clean JSON, never raw HTML.
743
+ *
744
+ * Requirement: 3.3
745
+ */
746
+ async extract(
747
+ context: ExecutionContext,
748
+ element: IntentElement
749
+ ): Promise<ActionResult> {
750
+ try {
751
+ // Log the extract action
752
+ this.logAction(context, 'extract', element.intentId, 'success');
753
+
754
+ return {
755
+ success: true,
756
+ data: {
757
+ intentId: element.intentId,
758
+ label: element.label,
759
+ role: element.role,
760
+ },
761
+ };
762
+ } catch (error) {
763
+ this.logAction(context, 'extract', element.intentId, 'failure');
764
+ return {
765
+ success: false,
766
+ error: error instanceof Error ? error.message : 'Extract failed',
767
+ };
768
+ }
769
+ }
770
+
771
+ /**
772
+ * Execute a sequence of DOM actions transparently.
773
+ * Agent provides high-level intent, OmniBridge handles the details.
774
+ *
775
+ * Requirement: 3.3
776
+ */
777
+ async executeActions(
778
+ context: ExecutionContext,
779
+ document: IntentDocument,
780
+ actions: Array<{
781
+ type: 'click' | 'type' | 'navigate' | 'wait' | 'extract';
782
+ target?: string; // Intent ID or URL
783
+ value?: string;
784
+ }>
785
+ ): Promise<ActionResult[]> {
786
+ const results: ActionResult[] = [];
787
+
788
+ for (const action of actions) {
789
+ let result: ActionResult;
790
+
791
+ switch (action.type) {
792
+ case 'click': {
793
+ const element = this.findElementByIntentId(document, action.target || '');
794
+ if (element) {
795
+ result = await this.click(context, element);
796
+ } else {
797
+ result = { success: false, error: `Element not found: ${action.target}` };
798
+ }
799
+ break;
800
+ }
801
+ case 'type': {
802
+ const element = this.findElementByIntentId(document, action.target || '');
803
+ if (element) {
804
+ result = await this.type(context, element, action.value || '');
805
+ } else {
806
+ result = { success: false, error: `Element not found: ${action.target}` };
807
+ }
808
+ break;
809
+ }
810
+ case 'navigate':
811
+ result = await this.navigate(context, action.target || '');
812
+ break;
813
+ case 'wait':
814
+ result = await this.wait(context, action.target || 'page_load');
815
+ break;
816
+ case 'extract': {
817
+ const element = this.findElementByIntentId(document, action.target || '');
818
+ if (element) {
819
+ result = await this.extract(context, element);
820
+ } else {
821
+ result = { success: false, error: `Element not found: ${action.target}` };
822
+ }
823
+ break;
824
+ }
825
+ default:
826
+ result = { success: false, error: `Unknown action type: ${action.type}` };
827
+ }
828
+
829
+ results.push(result);
830
+
831
+ // Stop on failure
832
+ if (!result.success) {
833
+ break;
834
+ }
835
+ }
836
+
837
+ return results;
838
+ }
839
+
840
+ /**
841
+ * Find an element by its Intent ID.
842
+ */
843
+ private findElementByIntentId(
844
+ document: IntentDocument,
845
+ intentId: string
846
+ ): IntentElement | undefined {
847
+ return document.elements.find(el => el.intentId === intentId);
848
+ }
849
+
850
+ // ===========================================================================
851
+ // Pagination Handling (Requirement 3.5)
852
+ // ===========================================================================
853
+
854
+ /**
855
+ * Detect pagination in an Intent Document.
856
+ *
857
+ * Requirement: 3.5
858
+ */
859
+ detectPagination(document: IntentDocument): PaginationInfo {
860
+ const paginationKeywords = [
861
+ 'next', 'previous', 'prev', 'page', 'pagination',
862
+ 'more', 'load more', 'show more', 'view more',
863
+ '»', '›', '‹', '«', '→', '←',
864
+ ];
865
+
866
+ const nextPageIndicators = [
867
+ 'next', '»', '›', '→', 'next page', 'next →', '→ next',
868
+ 'load more', 'show more', 'view more', 'see more',
869
+ ];
870
+
871
+ let nextPageElement: IntentElement | undefined;
872
+ let hasPagination = false;
873
+ let totalPages: number | undefined;
874
+
875
+ // Look for pagination elements
876
+ for (const element of document.elements) {
877
+ const labelLower = element.label.toLowerCase();
878
+ const intentLower = element.intentId.toLowerCase();
879
+
880
+ // Check for next page indicators
881
+ if (nextPageIndicators.some(indicator =>
882
+ labelLower.includes(indicator) || labelLower === indicator
883
+ )) {
884
+ nextPageElement = element;
885
+ hasPagination = true;
886
+ }
887
+
888
+ // Check for pagination container
889
+ if (paginationKeywords.some(kw => labelLower.includes(kw) || intentLower.includes(kw))) {
890
+ hasPagination = true;
891
+ }
892
+
893
+ // Try to detect total pages from labels like "Page 1 of 10"
894
+ const pageMatch = labelLower.match(/page\s*\d+\s*of\s*(\d+)/);
895
+ if (pageMatch) {
896
+ totalPages = parseInt(pageMatch[1], 10);
897
+ hasPagination = true;
898
+ }
899
+
900
+ // Check for numbered page links
901
+ const pageNumberMatch = labelLower.match(/^(\d+)$/);
902
+ if (pageNumberMatch && element.role === 'navigation') {
903
+ hasPagination = true;
904
+ const pageNum = parseInt(pageNumberMatch[1], 10);
905
+ if (!totalPages || pageNum > totalPages) {
906
+ totalPages = pageNum;
907
+ }
908
+ }
909
+ }
910
+
911
+ // Check navigation links for pagination
912
+ for (const link of document.navigation.primaryLinks) {
913
+ const labelLower = link.label.toLowerCase();
914
+ if (nextPageIndicators.some(indicator => labelLower.includes(indicator))) {
915
+ hasPagination = true;
916
+ }
917
+ if (labelLower.includes('page') || /^\d+$/.test(labelLower)) {
918
+ hasPagination = true;
919
+ }
920
+ }
921
+
922
+ return {
923
+ hasPagination,
924
+ nextPageElement,
925
+ totalPages,
926
+ currentPage: 1,
927
+ };
928
+ }
929
+
930
+ /**
931
+ * Handle pagination automatically.
932
+ * Collects data from multiple pages.
933
+ *
934
+ * Requirement: 3.5
935
+ */
936
+ async handlePagination(
937
+ context: ExecutionContext,
938
+ document: IntentDocument,
939
+ paginationInfo: PaginationInfo
940
+ ): Promise<Record<string, unknown>[]> {
941
+ const allData: Record<string, unknown>[] = [];
942
+
943
+ // Extract data from current page
944
+ const currentPageData = this.extractData(context, document);
945
+ allData.push(...currentPageData);
946
+
947
+ // In a real implementation, we would:
948
+ // 1. Click the next page element
949
+ // 2. Wait for page load
950
+ // 3. Normalize the new HTML
951
+ // 4. Extract data
952
+ // 5. Repeat until no more pages or max reached
953
+
954
+ // For now, we just log that pagination was detected
955
+ if (paginationInfo.hasPagination && paginationInfo.nextPageElement) {
956
+ this.logAction(
957
+ context,
958
+ 'click',
959
+ paginationInfo.nextPageElement.intentId,
960
+ 'success'
961
+ );
962
+ context.currentPage++;
963
+ }
964
+
965
+ return allData;
966
+ }
967
+
968
+ // ===========================================================================
969
+ // Response Formatting (Requirements 3.4, 3.6)
970
+ // ===========================================================================
971
+
972
+ /**
973
+ * Create a Ghost-API response with proper formatting.
974
+ *
975
+ * Requirements: 3.4, 3.6
976
+ */
977
+ createResponse(
978
+ context: ExecutionContext,
979
+ data: Record<string, unknown>[]
980
+ ): GhostResponse {
981
+ const executionTimeMs = performance.now() - context.startTime;
982
+
983
+ // Calculate confidence based on actions and heals
984
+ const confidence = this.calculateConfidence(context);
985
+
986
+ // Generate verification hash
987
+ const verificationHash = this.generateVerificationHash(context);
988
+
989
+ return {
990
+ data: data.length === 1 ? data[0] : data,
991
+ metadata: {
992
+ confidence,
993
+ executionTimeMs: Math.round(executionTimeMs * 100) / 100,
994
+ actionsPerformed: context.actionsPerformed,
995
+ triangulationHeals: context.triangulationHeals,
996
+ },
997
+ verificationHash,
998
+ };
999
+ }
1000
+
1001
+ /**
1002
+ * Calculate confidence score for the execution.
1003
+ */
1004
+ private calculateConfidence(context: ExecutionContext): number {
1005
+ let confidence = 1.0;
1006
+
1007
+ // Reduce confidence for each triangulation heal
1008
+ confidence -= context.triangulationHeals * 0.05;
1009
+
1010
+ // Reduce confidence if no data was extracted
1011
+ if (context.extractedData.length === 0) {
1012
+ confidence -= 0.2;
1013
+ }
1014
+
1015
+ // Reduce confidence if very few actions were performed
1016
+ if (context.actionsPerformed < 2) {
1017
+ confidence -= 0.1;
1018
+ }
1019
+
1020
+ // Ensure confidence is between 0 and 1
1021
+ return Math.max(0, Math.min(1, confidence));
1022
+ }
1023
+
1024
+ /**
1025
+ * Generate a verification hash for the execution.
1026
+ */
1027
+ private generateVerificationHash(context: ExecutionContext): string {
1028
+ const log = this.logger.getLog(context.sessionId);
1029
+
1030
+ if (log.length === 0) {
1031
+ // Generate hash from context if no log
1032
+ const data = JSON.stringify({
1033
+ sessionId: context.sessionId,
1034
+ goal: context.goal.name,
1035
+ actionsPerformed: context.actionsPerformed,
1036
+ timestamp: Date.now(),
1037
+ });
1038
+ return createHash('sha256').update(data).digest('hex');
1039
+ }
1040
+
1041
+ return this.logger.hashActionSequence(log);
1042
+ }
1043
+
1044
+ // ===========================================================================
1045
+ // Utility Methods
1046
+ // ===========================================================================
1047
+
1048
+ /**
1049
+ * Clear all registered goals.
1050
+ */
1051
+ clearGoals(): void {
1052
+ this.goals.clear();
1053
+ }
1054
+
1055
+ /**
1056
+ * Get the number of registered goals.
1057
+ */
1058
+ getGoalCount(): number {
1059
+ return this.goals.size;
1060
+ }
1061
+
1062
+ /**
1063
+ * Get configuration values.
1064
+ */
1065
+ getConfig(): {
1066
+ basePath: string;
1067
+ maxPaginationPages: number;
1068
+ executionTimeoutMs: number;
1069
+ } {
1070
+ return {
1071
+ basePath: this.basePath,
1072
+ maxPaginationPages: this.maxPaginationPages,
1073
+ executionTimeoutMs: this.executionTimeoutMs,
1074
+ };
1075
+ }
1076
+ }
1077
+
1078
+ // =============================================================================
1079
+ // Factory Function
1080
+ // =============================================================================
1081
+
1082
+ /**
1083
+ * Create a new Ghost-API instance.
1084
+ */
1085
+ export function createGhostApi(config: GhostApiConfig): GhostApi {
1086
+ return new GhostApi(config);
1087
+ }