@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.
- package/README.md +67 -8
- package/dist/agentic_runtime/patch/structured_patch.js +240 -0
- package/dist/agentic_runtime/workspace/manager.js +1044 -0
- package/dist/cli_terminal.js +490 -0
- package/dist/executors/native_macos.js +2778 -115
- package/dist/local_automations.js +18 -1
- package/dist/main.js +23 -0
- package/dist/runtime.js +91 -13
- package/dist/runtime_cli_client.js +18 -0
- package/dist/runtime_contract.js +516 -0
- package/dist/tool_catalog.js +148 -1
- package/dist/types.js +2 -2
- package/package.json +7 -2
- package/scripts/postinstall.mjs +35 -0
|
@@ -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 (
|
|
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.
|
|
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
|
|
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.
|
|
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
|
+
}
|