@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 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.0.tgz
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");
@@ -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
- await reporter.accepted();
8
- await reporter.progress(15, "Planejando execução local");
9
- await sleep(150);
10
- if (shouldRequireConfirmation(job.payload)) {
11
- const decision = await reporter.confirmRequired("Aguardando confirmação do usuário para continuar a execução mock", {
12
- step: "mock_confirmation_gate",
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
- payload_preview: job.payload,
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
- else {
22
- await reporter.progress(55, "Executando ação mock no dispositivo");
45
+ finally {
46
+ cancelledJobs.delete(job.job_id);
23
47
  }
24
- await sleep(150);
25
- await reporter.progress(90, "Finalizando resultado");
26
- await sleep(100);
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
  }
@@ -0,0 +1,6 @@
1
+ export class JobCancelledError extends Error {
2
+ constructor(jobId) {
3
+ super(`Job cancelled: ${jobId}`);
4
+ this.name = "JobCancelledError";
5
+ }
6
+ }
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.0";
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leg3ndy/otto-bridge",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Local companion for Otto Bridge device pairing and WebSocket runtime.",