@leg3ndy/otto-bridge 0.1.0 → 0.1.2

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:
@@ -20,13 +24,14 @@ O pacote ja esta estruturado para install via CLI:
20
24
  ```bash
21
25
  npm install -g @leg3ndy/otto-bridge
22
26
  otto-bridge status
27
+ otto-bridge version
23
28
  ```
24
29
 
25
30
  Enquanto o pacote nao estiver publicado, voce pode gerar um tarball local:
26
31
 
27
32
  ```bash
28
33
  npm pack
29
- npm install -g ./otto-bridge-0.1.0.tgz
34
+ npm install -g ./leg3ndy-otto-bridge-0.1.2.tgz
30
35
  ```
31
36
 
32
37
  ## Publicacao
@@ -51,6 +56,13 @@ npm_config_cache=/tmp/otto-npm-cache npm publish --access public
51
56
 
52
57
  ## Comandos
53
58
 
59
+ ### Listar ajuda
60
+
61
+ ```bash
62
+ otto-bridge help
63
+ otto-bridge --help
64
+ ```
65
+
54
66
  ### Parear o dispositivo
55
67
 
56
68
  ```bash
@@ -80,6 +92,33 @@ Se o executor estiver salvo no `config.json`, o `run` usa essa configuracao por
80
92
  otto-bridge status
81
93
  ```
82
94
 
95
+ ### Ver versao instalada
96
+
97
+ ```bash
98
+ otto-bridge version
99
+ otto-bridge --version
100
+ ```
101
+
102
+ ### Atualizar o pacote
103
+
104
+ Atualizacao automatica via npm:
105
+
106
+ ```bash
107
+ otto-bridge update
108
+ ```
109
+
110
+ Para apenas ver qual comando sera executado:
111
+
112
+ ```bash
113
+ otto-bridge update --dry-run
114
+ ```
115
+
116
+ Para instalar manualmente:
117
+
118
+ ```bash
119
+ npm install -g @leg3ndy/otto-bridge@latest
120
+ ```
121
+
83
122
  ### Remover pareamento local
84
123
 
85
124
  ```bash
@@ -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
@@ -1,11 +1,18 @@
1
1
  #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
2
3
  import process from "node:process";
3
4
  import { clearBridgeConfig, getBridgeConfigPath, loadBridgeConfig, resolveApiBaseUrl, resolveExecutorConfig, } from "./config.js";
4
5
  import { pairDevice } from "./pairing.js";
5
6
  import { BridgeRuntime } from "./runtime.js";
6
- import { DEFAULT_PAIR_TIMEOUT_SECONDS, DEFAULT_POLL_INTERVAL_MS, } from "./types.js";
7
+ import { BRIDGE_PACKAGE_NAME, BRIDGE_VERSION, DEFAULT_PAIR_TIMEOUT_SECONDS, DEFAULT_POLL_INTERVAL_MS, } from "./types.js";
7
8
  function parseArgs(argv) {
8
9
  const [maybeCommand, ...rest] = argv;
10
+ if (maybeCommand === "--help" || maybeCommand === "-h") {
11
+ return { command: "help", options: new Map() };
12
+ }
13
+ if (maybeCommand === "--version" || maybeCommand === "-v") {
14
+ return { command: "version", options: new Map() };
15
+ }
9
16
  const command = maybeCommand && !maybeCommand.startsWith("--") ? maybeCommand : "run";
10
17
  const tokens = command === "run" && maybeCommand?.startsWith("--") ? argv : rest;
11
18
  const options = new Map();
@@ -49,7 +56,38 @@ function printUsage() {
49
56
  otto-bridge pair --api http://localhost:8000 --code ABC123 [--name "Meu PC"] [--executor mock|clawd-cursor]
50
57
  otto-bridge run [--executor mock|clawd-cursor] [--clawd-url http://127.0.0.1:3847]
51
58
  otto-bridge status
52
- otto-bridge unpair`);
59
+ otto-bridge version
60
+ otto-bridge update [--tag latest|next] [--dry-run]
61
+ otto-bridge unpair
62
+
63
+ Examples:
64
+ otto-bridge pair --api https://api.leg3ndy.com.br --code ABC123 --executor clawd-cursor --clawd-url http://127.0.0.1:3847
65
+ otto-bridge run
66
+ otto-bridge version
67
+ otto-bridge update
68
+ otto-bridge update --dry-run
69
+ otto-bridge --version`);
70
+ }
71
+ function printVersion() {
72
+ console.log(`${BRIDGE_PACKAGE_NAME} ${BRIDGE_VERSION}`);
73
+ }
74
+ function runChildCommand(command, args) {
75
+ return new Promise((resolve, reject) => {
76
+ const child = spawn(command, args, {
77
+ stdio: "inherit",
78
+ env: process.env,
79
+ });
80
+ child.on("error", (error) => {
81
+ reject(error);
82
+ });
83
+ child.on("exit", (code) => {
84
+ if (code === 0) {
85
+ resolve();
86
+ return;
87
+ }
88
+ reject(new Error(`${command} exited with code ${code ?? "unknown"}`));
89
+ });
90
+ });
53
91
  }
54
92
  async function runPairCommand(args) {
55
93
  const code = option(args, "code");
@@ -68,6 +106,7 @@ async function runPairCommand(args) {
68
106
  console.log(`[otto-bridge] paired device=${config.deviceId}`);
69
107
  console.log(`[otto-bridge] executor=${config.executor.type}`);
70
108
  console.log(`[otto-bridge] config=${getBridgeConfigPath()}`);
109
+ console.log("[otto-bridge] next step: run `otto-bridge run` to keep this device online");
71
110
  }
72
111
  async function runRuntimeCommand(args) {
73
112
  const config = await loadBridgeConfig();
@@ -105,6 +144,23 @@ async function runUnpairCommand() {
105
144
  await clearBridgeConfig();
106
145
  console.log("[otto-bridge] local pairing cleared");
107
146
  }
147
+ async function runUpdateCommand(args) {
148
+ const tag = option(args, "tag") || "latest";
149
+ const packageSpec = `${BRIDGE_PACKAGE_NAME}@${tag}`;
150
+ const command = process.platform === "win32" ? "npm.cmd" : "npm";
151
+ const commandArgs = ["install", "-g", packageSpec];
152
+ const commandString = `${command} ${commandArgs.join(" ")}`;
153
+ if (args.options.has("dry-run") || args.options.has("check")) {
154
+ console.log(`[otto-bridge] current=${BRIDGE_VERSION}`);
155
+ console.log(`[otto-bridge] update command: ${commandString}`);
156
+ return;
157
+ }
158
+ console.log(`[otto-bridge] current=${BRIDGE_VERSION}`);
159
+ console.log(`[otto-bridge] updating with: ${commandString}`);
160
+ await runChildCommand(command, commandArgs);
161
+ console.log("[otto-bridge] update completed");
162
+ console.log("[otto-bridge] run `otto-bridge version` to confirm the installed version");
163
+ }
108
164
  async function main() {
109
165
  const args = parseArgs(process.argv.slice(2));
110
166
  switch (args.command) {
@@ -117,6 +173,16 @@ async function main() {
117
173
  case "status":
118
174
  await runStatusCommand();
119
175
  return;
176
+ case "version":
177
+ printVersion();
178
+ return;
179
+ case "--version":
180
+ case "-v":
181
+ printVersion();
182
+ return;
183
+ case "update":
184
+ await runUpdateCommand(args);
185
+ return;
120
186
  case "unpair":
121
187
  await runUnpairCommand();
122
188
  return;
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,6 @@
1
1
  export const BRIDGE_CONFIG_VERSION = 1;
2
- export const BRIDGE_VERSION = "0.1.0";
2
+ export const BRIDGE_VERSION = "0.1.2";
3
+ export const BRIDGE_PACKAGE_NAME = "@leg3ndy/otto-bridge";
3
4
  export const DEFAULT_API_BASE_URL = "http://localhost:8000";
4
5
  export const DEFAULT_POLL_INTERVAL_MS = 3000;
5
6
  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.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Local companion for Otto Bridge device pairing and WebSocket runtime.",
@@ -38,6 +38,8 @@
38
38
  "pair": "node dist/main.js pair",
39
39
  "run": "node dist/main.js run",
40
40
  "status": "node dist/main.js status",
41
+ "version:cli": "node dist/main.js version",
42
+ "update:cli": "node dist/main.js update --dry-run",
41
43
  "unpair": "node dist/main.js unpair"
42
44
  },
43
45
  "publishConfig": {