@leg3ndy/otto-bridge 0.9.2 → 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.
@@ -231,6 +231,9 @@ function isSupportedWhatsAppInboxAutomation(automation) {
231
231
  const bridgeConfig = automation.bridge_config || {};
232
232
  return String(bridgeConfig.monitor_scope || "").trim().toLowerCase() === "inbox";
233
233
  }
234
+ function isSupportedBridgeAutomation(automation) {
235
+ return normalizeChannel(automation.channel) === "bridge";
236
+ }
234
237
  export class LocalAutomationRuntime {
235
238
  config;
236
239
  automations = new Map();
@@ -353,7 +356,10 @@ export class LocalAutomationRuntime {
353
356
  }
354
357
  state.running = true;
355
358
  try {
356
- if (isSupportedWhatsAppContactAutomation(automation)) {
359
+ if (isSupportedBridgeAutomation(automation)) {
360
+ await this.handleBridgeAutomation(automation);
361
+ }
362
+ else if (isSupportedWhatsAppContactAutomation(automation)) {
357
363
  await this.handleWhatsAppContactAutomation(automation, state);
358
364
  }
359
365
  else if (isSupportedWhatsAppInboxAutomation(automation)) {
@@ -438,6 +444,17 @@ export class LocalAutomationRuntime {
438
444
  }
439
445
  }
440
446
  }
447
+ async handleBridgeAutomation(automation) {
448
+ const automationId = String(automation.id || "").trim();
449
+ if (!automationId) {
450
+ return;
451
+ }
452
+ await postDeviceJson(this.config.apiBaseUrl, this.config.deviceToken, "/v1/devices/automations/local/bridge/trigger", {
453
+ automation_id: automationId,
454
+ observed_at: new Date().toISOString(),
455
+ reason: "schedule_tick",
456
+ });
457
+ }
441
458
  isPrimaryContactForAutomation(automation, contact) {
442
459
  if (!isSupportedWhatsAppContactAutomation(automation)) {
443
460
  return false;
package/dist/main.js CHANGED
@@ -7,10 +7,15 @@ import { pairDevice } from "./pairing.js";
7
7
  import { BridgeRuntime } from "./runtime.js";
8
8
  import { detectWhatsAppBackgroundStatus, runWhatsAppBackgroundSetup, } from "./whatsapp_background.js";
9
9
  import { BRIDGE_PACKAGE_NAME, BRIDGE_VERSION, DEFAULT_PAIR_TIMEOUT_SECONDS, DEFAULT_POLL_INTERVAL_MS, } from "./types.js";
10
+ import { launchInteractiveCli, runConsoleCommand, runSetupCommand, } from "./cli_terminal.js";
10
11
  const RUNTIME_STATUS_FRESHNESS_MS = 90_000;
11
12
  const UPDATE_RETRY_DELAYS_MS = [0, 8_000, 20_000];
12
13
  function parseArgs(argv) {
13
14
  const [maybeCommand, ...rest] = argv;
15
+ if (!maybeCommand) {
16
+ const interactiveDefault = process.stdout.isTTY && process.stdin.isTTY && process.env.OTTO_BRIDGE_LEGACY_DEFAULT_RUN !== "1";
17
+ return { command: interactiveDefault ? "home" : "run", options: new Map() };
18
+ }
14
19
  if (maybeCommand === "--help" || maybeCommand === "-h") {
15
20
  return { command: "help", options: new Map() };
16
21
  }
@@ -100,6 +105,10 @@ function resolveExecutorOverrides(args, current) {
100
105
  }
101
106
  function printUsage() {
102
107
  console.log(`Usage:
108
+ otto-bridge
109
+ otto-bridge home
110
+ otto-bridge setup
111
+ otto-bridge console
103
112
  otto-bridge pair --api http://localhost:8000 --code ABC123 [--name "Meu PC"] [--executor native-macos|mock|clawd-cursor]
104
113
  otto-bridge run [--executor native-macos|mock|clawd-cursor] [--clawd-url http://127.0.0.1:3847]
105
114
  otto-bridge status
@@ -113,6 +122,9 @@ function printUsage() {
113
122
  otto-bridge unpair
114
123
 
115
124
  Examples:
125
+ otto-bridge
126
+ otto-bridge setup
127
+ otto-bridge console
116
128
  otto-bridge pair --api https://api.leg3ndy.com.br --code ABC123
117
129
  otto-bridge run
118
130
  otto-bridge extensions --install whatsappweb
@@ -574,6 +586,17 @@ async function runUpdateCommand(args) {
574
586
  async function main() {
575
587
  const args = parseArgs(process.argv.slice(2));
576
588
  switch (args.command) {
589
+ case "home":
590
+ await launchInteractiveCli();
591
+ return;
592
+ case "setup":
593
+ await runSetupCommand({
594
+ postinstall: args.options.has("postinstall"),
595
+ });
596
+ return;
597
+ case "console":
598
+ await runConsoleCommand(option(args, "prompt"));
599
+ return;
577
600
  case "pair":
578
601
  await runPairCommand(args);
579
602
  return;
package/dist/runtime.js CHANGED
@@ -6,6 +6,7 @@ import { JobCancelledError } from "./executors/shared.js";
6
6
  import { isManagedBridgeExtensionSlug, loadManagedBridgeExtensionState, } from "./extensions.js";
7
7
  import { LocalAutomationRuntime } from "./local_automations.js";
8
8
  import { buildLocalToolCatalog } from "./tool_catalog.js";
9
+ import { parseJobRuntimeManifest, runtimeStepIdForEvent, } from "./runtime_contract.js";
9
10
  function delay(ms) {
10
11
  return new Promise((resolve) => setTimeout(resolve, ms));
11
12
  }
@@ -55,6 +56,11 @@ function bridgeReleaseFromMessage(message) {
55
56
  }
56
57
  return null;
57
58
  }
59
+ function confirmationKey(jobId, stepId) {
60
+ const normalizedJobId = String(jobId || "").trim();
61
+ const normalizedStepId = String(stepId || "").trim();
62
+ return normalizedStepId ? `${normalizedJobId}:${normalizedStepId}` : normalizedJobId;
63
+ }
58
64
  async function parseSocketMessage(data) {
59
65
  if (typeof data === "string") {
60
66
  return JSON.parse(data);
@@ -306,7 +312,7 @@ export class BridgeRuntime {
306
312
  this.resolveConfirmation(message);
307
313
  return;
308
314
  case "device.job.cancel":
309
- await this.cancelJob(String(message.job_id || ""));
315
+ await this.cancelJob(String(message.job_id || ""), String(message.step_id || ""));
310
316
  return;
311
317
  default:
312
318
  console.log(`[otto-bridge] event=${type || "unknown"} payload=${JSON.stringify(message)}`);
@@ -335,42 +341,95 @@ export class BridgeRuntime {
335
341
  console.log(`[otto-bridge] update available current=${this.config.bridgeVersion} latest=${latestVersion || "unknown"} command="${updateCommand}"`);
336
342
  }
337
343
  }
344
+ clearPendingConfirmations(jobId) {
345
+ const normalizedJobId = String(jobId || "").trim();
346
+ if (!normalizedJobId) {
347
+ return;
348
+ }
349
+ for (const key of Array.from(this.pendingConfirmations.keys())) {
350
+ if (key === normalizedJobId || key.startsWith(`${normalizedJobId}:`)) {
351
+ this.pendingConfirmations.delete(key);
352
+ }
353
+ }
354
+ }
355
+ resolvePendingCancellation(jobId, stepId) {
356
+ const normalizedJobId = String(jobId || "").trim();
357
+ if (!normalizedJobId) {
358
+ return false;
359
+ }
360
+ const keys = stepId
361
+ ? [confirmationKey(normalizedJobId, stepId), normalizedJobId]
362
+ : Array.from(this.pendingConfirmations.keys()).filter((key) => (key === normalizedJobId || key.startsWith(`${normalizedJobId}:`)));
363
+ let resolved = false;
364
+ for (const key of keys) {
365
+ const waiter = this.pendingConfirmations.get(key);
366
+ if (!waiter) {
367
+ continue;
368
+ }
369
+ this.pendingConfirmations.delete(key);
370
+ waiter.resolve({
371
+ action: "reject",
372
+ note: "Cancelled by Otto Bridge",
373
+ });
374
+ resolved = true;
375
+ }
376
+ return resolved;
377
+ }
338
378
  resolveConfirmation(message) {
339
379
  const jobId = String(message.job_id || "");
380
+ const stepId = String(message.step_id || "");
340
381
  const action = String(message.action || "").trim().toLowerCase();
341
- const waiter = this.pendingConfirmations.get(jobId);
382
+ const waiter = this.pendingConfirmations.get(confirmationKey(jobId, stepId))
383
+ || this.pendingConfirmations.get(jobId);
342
384
  if (!jobId || !waiter) {
343
385
  console.warn(`[otto-bridge] unexpected confirmation payload=${JSON.stringify(message)}`);
344
386
  return;
345
387
  }
346
388
  if (action !== "approve" && action !== "reject") {
347
389
  waiter.reject(new Error(`Unsupported confirmation action: ${action || "unknown"}`));
390
+ this.pendingConfirmations.delete(confirmationKey(jobId, stepId));
348
391
  this.pendingConfirmations.delete(jobId);
349
392
  return;
350
393
  }
394
+ this.pendingConfirmations.delete(confirmationKey(jobId, stepId));
351
395
  this.pendingConfirmations.delete(jobId);
352
396
  waiter.resolve({
353
397
  action,
354
398
  note: typeof message.note === "string" ? message.note : undefined,
355
399
  });
356
400
  }
357
- async waitForConfirmation(jobId) {
401
+ async waitForConfirmation(jobId, stepId) {
358
402
  return await new Promise((resolve, reject) => {
359
- this.pendingConfirmations.set(jobId, { resolve, reject });
403
+ this.pendingConfirmations.set(confirmationKey(jobId, stepId), { resolve, reject });
360
404
  });
361
405
  }
362
- async cancelJob(jobId) {
406
+ async cancelJob(jobId, stepId) {
363
407
  if (!jobId) {
364
408
  return;
365
409
  }
410
+ if (this.resolvePendingCancellation(jobId, stepId)) {
411
+ console.log(`[otto-bridge] confirmation cancelled job=${jobId}${stepId ? ` step=${stepId}` : ""}`);
412
+ return;
413
+ }
366
414
  const cancel = this.activeCancels.get(jobId);
367
415
  if (!cancel) {
368
416
  console.warn(`[otto-bridge] cancel requested for unknown job=${jobId}`);
369
417
  return;
370
418
  }
419
+ if (stepId) {
420
+ console.log(`[otto-bridge] cancel requested job=${jobId} step=${stepId}`);
421
+ }
371
422
  await cancel();
372
423
  }
373
424
  async executeJob(socket, job) {
425
+ const runtimeManifest = parseJobRuntimeManifest(job);
426
+ const eventStepId = (eventType, options) => {
427
+ const explicitStepId = String(options?.stepId || "").trim();
428
+ if (explicitStepId) {
429
+ return explicitStepId;
430
+ }
431
+ return runtimeStepIdForEvent(runtimeManifest, eventType);
432
+ };
374
433
  const sendJson = async (payload) => {
375
434
  if (socket.readyState !== WebSocket.OPEN) {
376
435
  throw new Error("Socket is not open");
@@ -378,7 +437,7 @@ export class BridgeRuntime {
378
437
  socket.send(JSON.stringify(payload));
379
438
  };
380
439
  this.activeCancels.set(job.job_id, async () => {
381
- this.pendingConfirmations.delete(job.job_id);
440
+ this.clearPendingConfirmations(job.job_id);
382
441
  if (typeof this.executor.cancel === "function") {
383
442
  await this.executor.cancel(job.job_id);
384
443
  }
@@ -386,53 +445,69 @@ export class BridgeRuntime {
386
445
  });
387
446
  try {
388
447
  await this.executor.run(job, {
389
- accepted: async () => {
448
+ accepted: async (options) => {
449
+ const stepId = eventStepId("accepted", options);
390
450
  await sendJson({
391
451
  type: "device.job.accepted",
392
452
  device_id: this.config.deviceId,
393
453
  job_id: job.job_id,
454
+ graph_id: runtimeManifest.graphId,
455
+ step_id: stepId,
394
456
  accepted_at: Date.now(),
395
457
  });
396
458
  },
397
- progress: async (progressPercent, progressMessage) => {
459
+ progress: async (progressPercent, progressMessage, options) => {
460
+ const stepId = eventStepId("progress", options);
398
461
  await sendJson({
399
462
  type: "device.job.progress",
400
463
  device_id: this.config.deviceId,
401
464
  job_id: job.job_id,
465
+ graph_id: runtimeManifest.graphId,
466
+ step_id: stepId,
402
467
  progress_percent: progressPercent,
403
468
  progress_message: progressMessage,
404
469
  });
405
470
  },
406
- confirmRequired: async (progressMessage, confirmationContext) => {
407
- const confirmationPromise = this.waitForConfirmation(job.job_id);
471
+ confirmRequired: async (progressMessage, confirmationContext, options) => {
472
+ const stepId = eventStepId("confirm_required", options);
473
+ const confirmationPromise = this.waitForConfirmation(job.job_id, stepId);
408
474
  try {
409
475
  await sendJson({
410
476
  type: "device.job.confirm_required",
411
477
  device_id: this.config.deviceId,
412
478
  job_id: job.job_id,
479
+ graph_id: runtimeManifest.graphId,
480
+ step_id: stepId,
413
481
  progress_message: progressMessage,
414
482
  confirmation_context: confirmationContext || {},
415
483
  });
416
484
  }
417
485
  catch (error) {
486
+ this.pendingConfirmations.delete(confirmationKey(job.job_id, stepId));
418
487
  this.pendingConfirmations.delete(job.job_id);
419
488
  throw error;
420
489
  }
421
490
  return await confirmationPromise;
422
491
  },
423
- completed: async (result) => {
492
+ completed: async (result, options) => {
493
+ const stepId = eventStepId("completed", options);
424
494
  await sendJson({
425
495
  type: "device.job.completed",
426
496
  device_id: this.config.deviceId,
427
497
  job_id: job.job_id,
498
+ graph_id: runtimeManifest.graphId,
499
+ step_id: stepId,
428
500
  result: result || {},
429
501
  });
430
502
  },
431
- failed: async (errorMessage, result) => {
503
+ failed: async (errorMessage, result, options) => {
504
+ const stepId = eventStepId("failed", options);
432
505
  await sendJson({
433
506
  type: "device.job.failed",
434
507
  device_id: this.config.deviceId,
435
508
  job_id: job.job_id,
509
+ graph_id: runtimeManifest.graphId,
510
+ step_id: stepId,
436
511
  error_message: errorMessage,
437
512
  result: result || {},
438
513
  });
@@ -440,16 +515,19 @@ export class BridgeRuntime {
440
515
  });
441
516
  }
442
517
  catch (error) {
443
- this.pendingConfirmations.delete(job.job_id);
518
+ this.clearPendingConfirmations(job.job_id);
444
519
  if (error instanceof JobCancelledError) {
445
520
  return;
446
521
  }
447
522
  const detail = error instanceof Error ? error.message : String(error);
523
+ const stepId = runtimeStepIdForEvent(runtimeManifest, "failed");
448
524
  try {
449
525
  await sendJson({
450
526
  type: "device.job.failed",
451
527
  device_id: this.config.deviceId,
452
528
  job_id: job.job_id,
529
+ graph_id: runtimeManifest.graphId,
530
+ step_id: stepId,
453
531
  error_message: detail || "Executor failed",
454
532
  result: {
455
533
  executor: this.config.executor.type,
@@ -0,0 +1,18 @@
1
+ import { getDeviceJson, postDeviceJson } from "./http.js";
2
+ export async function submitRuntimeCliAssistantPrompt(config, request) {
3
+ return await postDeviceJson(config.apiBaseUrl, config.deviceToken, "/v1/devices/cli/runtime/assistant", request);
4
+ }
5
+ export async function getRuntimeCliJob(config, jobId) {
6
+ return await getDeviceJson(config.apiBaseUrl, config.deviceToken, `/v1/devices/cli/runtime/jobs/${encodeURIComponent(jobId)}`);
7
+ }
8
+ export async function confirmRuntimeCliJob(config, jobId, action, note) {
9
+ return await postDeviceJson(config.apiBaseUrl, config.deviceToken, `/v1/devices/cli/runtime/jobs/${encodeURIComponent(jobId)}/confirm`, {
10
+ action,
11
+ note,
12
+ });
13
+ }
14
+ export async function cancelRuntimeCliJob(config, jobId, note) {
15
+ return await postDeviceJson(config.apiBaseUrl, config.deviceToken, `/v1/devices/cli/runtime/jobs/${encodeURIComponent(jobId)}/cancel`, {
16
+ note,
17
+ });
18
+ }