@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 +40 -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 +68 -2
- package/dist/runtime.js +29 -0
- package/dist/types.js +2 -1
- package/package.json +3 -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:
|
|
@@ -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.
|
|
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");
|
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
|
@@ -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
|
|
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.
|
|
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.
|
|
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": {
|