@leg3ndy/otto-bridge 0.1.0 → 0.1.1
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 +5 -1
- package/dist/executors/clawd_cursor.js +17 -1
- package/dist/executors/mock.js +40 -21
- package/dist/executors/shared.js +6 -0
- package/dist/main.js +1 -0
- package/dist/runtime.js +29 -0
- package/dist/types.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,10 @@ Companion local do Otto para:
|
|
|
7
7
|
- manter um WebSocket persistente com o backend
|
|
8
8
|
- executar jobs locais com `mock` ou `clawd-cursor`
|
|
9
9
|
|
|
10
|
+
## Guia de uso
|
|
11
|
+
|
|
12
|
+
Para um passo a passo de instalacao, pareamento, uso, desconexao e desinstalacao, veja [USER_GUIDE.md](https://github.com/LGCYYL/ottoai/blob/main/otto-bridge/USER_GUIDE.md).
|
|
13
|
+
|
|
10
14
|
## Distribuicao
|
|
11
15
|
|
|
12
16
|
Fluxo recomendado agora:
|
|
@@ -26,7 +30,7 @@ Enquanto o pacote nao estiver publicado, voce pode gerar um tarball local:
|
|
|
26
30
|
|
|
27
31
|
```bash
|
|
28
32
|
npm pack
|
|
29
|
-
npm install -g ./otto-bridge-0.1.
|
|
33
|
+
npm install -g ./leg3ndy-otto-bridge-0.1.1.tgz
|
|
30
34
|
```
|
|
31
35
|
|
|
32
36
|
## Publicacao
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
2
2
|
import { getJson, postJson } from "../http.js";
|
|
3
|
+
import { JobCancelledError } from "./shared.js";
|
|
3
4
|
const LOG_LIMIT = 200;
|
|
4
5
|
const COMPLETION_SETTLE_TIMEOUT_MS = 4000;
|
|
5
6
|
const COMPLETION_SETTLE_INTERVAL_MS = 500;
|
|
@@ -99,6 +100,7 @@ function stateStatus(state) {
|
|
|
99
100
|
}
|
|
100
101
|
export class ClawdCursorJobExecutor {
|
|
101
102
|
config;
|
|
103
|
+
cancelledJobs = new Set();
|
|
102
104
|
constructor(config) {
|
|
103
105
|
this.config = config;
|
|
104
106
|
}
|
|
@@ -109,7 +111,13 @@ export class ClawdCursorJobExecutor {
|
|
|
109
111
|
let lastProgressKey = "";
|
|
110
112
|
let lastProgressPercent = 0;
|
|
111
113
|
let sawActiveState = false;
|
|
114
|
+
const assertNotCancelled = () => {
|
|
115
|
+
if (this.cancelledJobs.has(job.job_id)) {
|
|
116
|
+
throw new JobCancelledError(job.job_id);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
112
119
|
try {
|
|
120
|
+
assertNotCancelled();
|
|
113
121
|
await this.assertHealthy();
|
|
114
122
|
const taskResponse = await postJson(this.config.baseUrl, "/task", { task });
|
|
115
123
|
if (taskResponse.ok !== true) {
|
|
@@ -119,6 +127,7 @@ export class ClawdCursorJobExecutor {
|
|
|
119
127
|
const taskId = asString(taskResponse.task_id) || "";
|
|
120
128
|
await reporter.accepted();
|
|
121
129
|
while (true) {
|
|
130
|
+
assertNotCancelled();
|
|
122
131
|
const statusResponse = await getJson(this.config.baseUrl, "/status");
|
|
123
132
|
const state = asRecord(statusResponse.state);
|
|
124
133
|
const status = stateStatus(state);
|
|
@@ -176,11 +185,18 @@ export class ClawdCursorJobExecutor {
|
|
|
176
185
|
}
|
|
177
186
|
}
|
|
178
187
|
catch (error) {
|
|
179
|
-
if (taskAccepted) {
|
|
188
|
+
if (taskAccepted && !(error instanceof JobCancelledError)) {
|
|
180
189
|
await this.abortSilently();
|
|
181
190
|
}
|
|
182
191
|
throw error;
|
|
183
192
|
}
|
|
193
|
+
finally {
|
|
194
|
+
this.cancelledJobs.delete(job.job_id);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async cancel(jobId) {
|
|
198
|
+
this.cancelledJobs.add(jobId);
|
|
199
|
+
await this.abortSilently();
|
|
184
200
|
}
|
|
185
201
|
async assertHealthy() {
|
|
186
202
|
const health = await getJson(this.config.baseUrl, "/health");
|
package/dist/executors/mock.js
CHANGED
|
@@ -1,33 +1,52 @@
|
|
|
1
1
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
2
|
+
import { JobCancelledError } from "./shared.js";
|
|
3
|
+
const cancelledJobs = new Set();
|
|
2
4
|
function shouldRequireConfirmation(payload) {
|
|
3
5
|
return payload.require_confirmation === true || payload.requireConfirmation === true;
|
|
4
6
|
}
|
|
5
7
|
export class MockJobExecutor {
|
|
6
8
|
async run(job, reporter) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
const assertNotCancelled = () => {
|
|
10
|
+
if (cancelledJobs.has(job.job_id)) {
|
|
11
|
+
throw new JobCancelledError(job.job_id);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
try {
|
|
15
|
+
assertNotCancelled();
|
|
16
|
+
await reporter.accepted();
|
|
17
|
+
await reporter.progress(15, "Planejando execução local");
|
|
18
|
+
await sleep(150);
|
|
19
|
+
assertNotCancelled();
|
|
20
|
+
if (shouldRequireConfirmation(job.payload)) {
|
|
21
|
+
const decision = await reporter.confirmRequired("Aguardando confirmação do usuário para continuar a execução mock", {
|
|
22
|
+
step: "mock_confirmation_gate",
|
|
23
|
+
job_type: job.job_type,
|
|
24
|
+
payload_preview: job.payload,
|
|
25
|
+
});
|
|
26
|
+
if (decision.action === "reject") {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
await reporter.progress(55, "Confirmação recebida. Retomando execução mock");
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
await reporter.progress(55, "Executando ação mock no dispositivo");
|
|
33
|
+
}
|
|
34
|
+
await sleep(150);
|
|
35
|
+
assertNotCancelled();
|
|
36
|
+
await reporter.progress(90, "Finalizando resultado");
|
|
37
|
+
await sleep(100);
|
|
38
|
+
assertNotCancelled();
|
|
39
|
+
await reporter.completed({
|
|
40
|
+
summary: "Mock executor finished successfully",
|
|
13
41
|
job_type: job.job_type,
|
|
14
|
-
|
|
42
|
+
echoed_payload: job.payload,
|
|
15
43
|
});
|
|
16
|
-
if (decision.action === "reject") {
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
await reporter.progress(55, "Confirmação recebida. Retomando execução mock");
|
|
20
44
|
}
|
|
21
|
-
|
|
22
|
-
|
|
45
|
+
finally {
|
|
46
|
+
cancelledJobs.delete(job.job_id);
|
|
23
47
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
await reporter.completed({
|
|
28
|
-
summary: "Mock executor finished successfully",
|
|
29
|
-
job_type: job.job_type,
|
|
30
|
-
echoed_payload: job.payload,
|
|
31
|
-
});
|
|
48
|
+
}
|
|
49
|
+
async cancel(jobId) {
|
|
50
|
+
cancelledJobs.add(jobId);
|
|
32
51
|
}
|
|
33
52
|
}
|
package/dist/main.js
CHANGED
|
@@ -68,6 +68,7 @@ async function runPairCommand(args) {
|
|
|
68
68
|
console.log(`[otto-bridge] paired device=${config.deviceId}`);
|
|
69
69
|
console.log(`[otto-bridge] executor=${config.executor.type}`);
|
|
70
70
|
console.log(`[otto-bridge] config=${getBridgeConfigPath()}`);
|
|
71
|
+
console.log("[otto-bridge] next step: run `otto-bridge run` to keep this device online");
|
|
71
72
|
}
|
|
72
73
|
async function runRuntimeCommand(args) {
|
|
73
74
|
const config = await loadBridgeConfig();
|
package/dist/runtime.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DEFAULT_HEARTBEAT_INTERVAL_MS, DEFAULT_RECONNECT_BASE_DELAY_MS, DEFAULT_RECONNECT_MAX_DELAY_MS, } from "./types.js";
|
|
2
2
|
import { ClawdCursorJobExecutor } from "./executors/clawd_cursor.js";
|
|
3
3
|
import { MockJobExecutor } from "./executors/mock.js";
|
|
4
|
+
import { JobCancelledError } from "./executors/shared.js";
|
|
4
5
|
function delay(ms) {
|
|
5
6
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
7
|
}
|
|
@@ -24,6 +25,7 @@ export class BridgeRuntime {
|
|
|
24
25
|
reconnectDelayMs = DEFAULT_RECONNECT_BASE_DELAY_MS;
|
|
25
26
|
executor;
|
|
26
27
|
pendingConfirmations = new Map();
|
|
28
|
+
activeCancels = new Map();
|
|
27
29
|
constructor(config, executor) {
|
|
28
30
|
this.config = config;
|
|
29
31
|
this.executor = executor ?? this.createDefaultExecutor(config);
|
|
@@ -135,6 +137,9 @@ export class BridgeRuntime {
|
|
|
135
137
|
case "device.job.confirmation":
|
|
136
138
|
this.resolveConfirmation(message);
|
|
137
139
|
return;
|
|
140
|
+
case "device.job.cancel":
|
|
141
|
+
await this.cancelJob(String(message.job_id || ""));
|
|
142
|
+
return;
|
|
138
143
|
default:
|
|
139
144
|
console.log(`[otto-bridge] event=${type || "unknown"} payload=${JSON.stringify(message)}`);
|
|
140
145
|
}
|
|
@@ -163,6 +168,17 @@ export class BridgeRuntime {
|
|
|
163
168
|
this.pendingConfirmations.set(jobId, { resolve, reject });
|
|
164
169
|
});
|
|
165
170
|
}
|
|
171
|
+
async cancelJob(jobId) {
|
|
172
|
+
if (!jobId) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const cancel = this.activeCancels.get(jobId);
|
|
176
|
+
if (!cancel) {
|
|
177
|
+
console.warn(`[otto-bridge] cancel requested for unknown job=${jobId}`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
await cancel();
|
|
181
|
+
}
|
|
166
182
|
async executeJob(socket, job) {
|
|
167
183
|
const sendJson = async (payload) => {
|
|
168
184
|
if (socket.readyState !== WebSocket.OPEN) {
|
|
@@ -170,6 +186,13 @@ export class BridgeRuntime {
|
|
|
170
186
|
}
|
|
171
187
|
socket.send(JSON.stringify(payload));
|
|
172
188
|
};
|
|
189
|
+
this.activeCancels.set(job.job_id, async () => {
|
|
190
|
+
this.pendingConfirmations.delete(job.job_id);
|
|
191
|
+
if (typeof this.executor.cancel === "function") {
|
|
192
|
+
await this.executor.cancel(job.job_id);
|
|
193
|
+
}
|
|
194
|
+
console.log(`[otto-bridge] job cancelled job_id=${job.job_id}`);
|
|
195
|
+
});
|
|
173
196
|
try {
|
|
174
197
|
await this.executor.run(job, {
|
|
175
198
|
accepted: async () => {
|
|
@@ -227,6 +250,9 @@ export class BridgeRuntime {
|
|
|
227
250
|
}
|
|
228
251
|
catch (error) {
|
|
229
252
|
this.pendingConfirmations.delete(job.job_id);
|
|
253
|
+
if (error instanceof JobCancelledError) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
230
256
|
const detail = error instanceof Error ? error.message : String(error);
|
|
231
257
|
try {
|
|
232
258
|
await sendJson({
|
|
@@ -244,6 +270,9 @@ export class BridgeRuntime {
|
|
|
244
270
|
}
|
|
245
271
|
throw error;
|
|
246
272
|
}
|
|
273
|
+
finally {
|
|
274
|
+
this.activeCancels.delete(job.job_id);
|
|
275
|
+
}
|
|
247
276
|
}
|
|
248
277
|
createDefaultExecutor(config) {
|
|
249
278
|
if (config.executor.type === "clawd-cursor") {
|
package/dist/types.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const BRIDGE_CONFIG_VERSION = 1;
|
|
2
|
-
export const BRIDGE_VERSION = "0.1.
|
|
2
|
+
export const BRIDGE_VERSION = "0.1.1";
|
|
3
3
|
export const DEFAULT_API_BASE_URL = "http://localhost:8000";
|
|
4
4
|
export const DEFAULT_POLL_INTERVAL_MS = 3000;
|
|
5
5
|
export const DEFAULT_PAIR_TIMEOUT_SECONDS = 600;
|