@leg3ndy/otto-bridge 0.9.0 → 0.9.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 +12 -7
- package/dist/executors/native_macos.js +462 -40
- package/dist/tool_catalog.js +17 -0
- package/dist/types.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,6 +15,8 @@ Para o estado atual da arquitetura, capacidades entregues, limitacoes e roadmap
|
|
|
15
15
|
|
|
16
16
|
Para o corte de arquitetura do `0.9.0`, veja [`leg3ndy-ai-backend/docs/OTTO_BRIDGE_0_9_0_RELEASE.md`](../leg3ndy-ai-backend/docs/OTTO_BRIDGE_0_9_0_RELEASE.md).
|
|
17
17
|
|
|
18
|
+
Para o patch atual `0.9.2`, com confirmacao bloqueante correta e exclusao segura via Lixeira, veja [`leg3ndy-ai-backend/docs/OTTO_BRIDGE_0_9_2_PATCH.md`](../leg3ndy-ai-backend/docs/OTTO_BRIDGE_0_9_2_PATCH.md).
|
|
19
|
+
|
|
18
20
|
## Distribuicao
|
|
19
21
|
|
|
20
22
|
Fluxo recomendado agora:
|
|
@@ -35,14 +37,14 @@ Enquanto o pacote nao estiver publicado, voce pode gerar um tarball local:
|
|
|
35
37
|
|
|
36
38
|
```bash
|
|
37
39
|
npm pack
|
|
38
|
-
npm install -g ./leg3ndy-otto-bridge-0.9.
|
|
40
|
+
npm install -g ./leg3ndy-otto-bridge-0.9.2.tgz
|
|
39
41
|
```
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
Na linha `0.9.2`, `playwright` segue como dependencia obrigatoria no `otto-bridge`. O primeiro `npm install -g @leg3ndy/otto-bridge` pode demorar mais porque instala o browser persistente usado pelo WhatsApp Web e pelos fluxos web em background do bridge.
|
|
42
44
|
|
|
43
|
-
No macOS,
|
|
45
|
+
No macOS, a linha `0.9.2` usa o provider `macos-helper`, um helper `WKWebView` sem Dock para o WhatsApp Web. O helper sobe com user-agent de Chrome moderno para evitar o bloqueio do WhatsApp ao detectar Safari/WebKit. O runtime antigo com Chromium/Playwright fica disponivel apenas como override explicito via `OTTO_BRIDGE_WHATSAPP_RUNTIME_PROVIDER=embedded-playwright`.
|
|
44
46
|
|
|
45
|
-
No nivel arquitetural, o `0.9.0`
|
|
47
|
+
No nivel arquitetural, o `0.9.0` marcou a mudanca de papel do bridge: ele publica tools e resultados estruturados para o Otto, em vez de injetar resposta pronta como caminho principal do chat. O `0.9.2` consolida esse caminho com confirmacao bloqueante correta, exclusao segura via Lixeira, planner LLM-first e leitura/listagem local sem truncamento silencioso.
|
|
46
48
|
|
|
47
49
|
## Publicacao
|
|
48
50
|
|
|
@@ -112,7 +114,7 @@ otto-bridge run --executor clawd-cursor --clawd-url http://127.0.0.1:3847
|
|
|
112
114
|
|
|
113
115
|
### WhatsApp Web em background
|
|
114
116
|
|
|
115
|
-
Fluxo recomendado
|
|
117
|
+
Fluxo recomendado na linha `0.9.2`:
|
|
116
118
|
|
|
117
119
|
```bash
|
|
118
120
|
otto-bridge extensions --install whatsappweb
|
|
@@ -122,13 +124,13 @@ otto-bridge extensions --status whatsappweb
|
|
|
122
124
|
|
|
123
125
|
O setup agora abre o login do WhatsApp Web no helper/background browser do proprio bridge. Depois do QR code, o Otto usa a sessao local em background, sem depender de aba visivel no Safari.
|
|
124
126
|
|
|
125
|
-
Contrato
|
|
127
|
+
Contrato da linha `0.9.2`:
|
|
126
128
|
|
|
127
129
|
- `otto-bridge extensions --setup whatsappweb`: autentica a sessao uma vez
|
|
128
130
|
- `otto-bridge run`: mantem o browser persistente do WhatsApp vivo em background enquanto o runtime estiver ativo, sem depender de uma aba aberta no Safari
|
|
129
131
|
- ao parar o `otto-bridge run`: o browser em background e desligado, mas a sessao local fica lembrada para o proximo boot
|
|
130
132
|
|
|
131
|
-
## Handoff rapido
|
|
133
|
+
## Handoff rapido da linha 0.9.2
|
|
132
134
|
|
|
133
135
|
Ja fechado no codigo:
|
|
134
136
|
|
|
@@ -138,6 +140,9 @@ Ja fechado no codigo:
|
|
|
138
140
|
- prompt bridge-aware no chat normal para ajudar o Otto a responder com base no que realmente aconteceu no device
|
|
139
141
|
- runtime local agora publica `local_tools` para o Otto/backend saberem exatamente o que o device consegue fazer
|
|
140
142
|
- o caminho principal do bridge usa `summary`/`narration_context` no lugar de resposta automatica pronta
|
|
143
|
+
- `read_file` agora entrega conteudo completo segmentado em `content_chunks` para o Otto
|
|
144
|
+
- `list_files` sem `limit` agora lista o diretorio inteiro, sem fallback silencioso para 40 itens
|
|
145
|
+
- o planner do bridge prioriza o modelo para escolher a tool certa pelo contexto; regex fica como fallback
|
|
141
146
|
|
|
142
147
|
Ainda precisa reteste em campo:
|
|
143
148
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { mkdir, readFile, readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, readdir, rename, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import process from "node:process";
|
|
@@ -702,6 +702,46 @@ function clipTextPreview(value, maxLength) {
|
|
|
702
702
|
}
|
|
703
703
|
return `${value.slice(0, maxLength)}\n\n[conteudo truncado: mostrando ${maxLength} de ${value.length} caracteres. Peca um trecho mais especifico se quiser continuar.]`;
|
|
704
704
|
}
|
|
705
|
+
function sanitizeFileName(value, fallback = "otto-note.txt") {
|
|
706
|
+
const normalized = String(value || "")
|
|
707
|
+
.normalize("NFD")
|
|
708
|
+
.replace(/\p{Diacritic}/gu, "")
|
|
709
|
+
.replace(/[\/\\?%*:|"<>]/g, "-")
|
|
710
|
+
.replace(/\s+/g, " ")
|
|
711
|
+
.trim();
|
|
712
|
+
const collapsed = normalized.replace(/\.+/g, ".").replace(/^\.+/, "").replace(/\.+$/, "");
|
|
713
|
+
const candidate = collapsed || fallback;
|
|
714
|
+
return path.extname(candidate) ? candidate : `${candidate}.txt`;
|
|
715
|
+
}
|
|
716
|
+
function chunkTextForTransport(value, chunkSize) {
|
|
717
|
+
const normalized = String(value || "");
|
|
718
|
+
if (!normalized) {
|
|
719
|
+
return [];
|
|
720
|
+
}
|
|
721
|
+
const safeChunkSize = Math.max(400, Math.min(Math.round(chunkSize || 4000), 32_000));
|
|
722
|
+
const chunks = [];
|
|
723
|
+
let cursor = 0;
|
|
724
|
+
let index = 0;
|
|
725
|
+
while (cursor < normalized.length) {
|
|
726
|
+
let nextCursor = Math.min(normalized.length, cursor + safeChunkSize);
|
|
727
|
+
if (nextCursor < normalized.length) {
|
|
728
|
+
const lastLineBreak = normalized.lastIndexOf("\n", nextCursor);
|
|
729
|
+
if (lastLineBreak > cursor + Math.floor(safeChunkSize * 0.35)) {
|
|
730
|
+
nextCursor = lastLineBreak + 1;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
const text = normalized.slice(cursor, nextCursor);
|
|
734
|
+
chunks.push({
|
|
735
|
+
index,
|
|
736
|
+
start_char: cursor,
|
|
737
|
+
end_char: nextCursor,
|
|
738
|
+
text,
|
|
739
|
+
});
|
|
740
|
+
cursor = nextCursor;
|
|
741
|
+
index += 1;
|
|
742
|
+
}
|
|
743
|
+
return chunks;
|
|
744
|
+
}
|
|
705
745
|
const TEXTUTIL_READABLE_EXTENSIONS = new Set([
|
|
706
746
|
".doc",
|
|
707
747
|
".docx",
|
|
@@ -975,14 +1015,38 @@ function parseStructuredActions(job) {
|
|
|
975
1015
|
if (type === "read_file" || type === "read_local_file") {
|
|
976
1016
|
const filePath = asString(action.path);
|
|
977
1017
|
if (filePath) {
|
|
978
|
-
const maxChars = typeof action.max_chars === "number" ? Math.max(
|
|
1018
|
+
const maxChars = typeof action.max_chars === "number" ? Math.max(400, Math.min(32_000, action.max_chars)) : undefined;
|
|
979
1019
|
actions.push({ type: "read_file", path: filePath, max_chars: maxChars });
|
|
980
1020
|
}
|
|
981
1021
|
continue;
|
|
982
1022
|
}
|
|
1023
|
+
if (type === "write_text_file" || type === "write_file" || type === "save_text_file" || type === "save_file") {
|
|
1024
|
+
const targetPath = asString(action.path)
|
|
1025
|
+
|| asString(action.destination)
|
|
1026
|
+
|| asString(action.file_path)
|
|
1027
|
+
|| asString(action.target)
|
|
1028
|
+
|| asString(action.directory)
|
|
1029
|
+
|| asString(action.folder)
|
|
1030
|
+
|| (asString(action.filename) ? "~/Desktop" : "");
|
|
1031
|
+
const text = asString(action.text) || asString(action.content) || asString(action.body) || asString(action.value);
|
|
1032
|
+
const source = asString(action.source) || asString(action.input_source);
|
|
1033
|
+
if (targetPath && (text || source)) {
|
|
1034
|
+
actions.push({
|
|
1035
|
+
type: "write_text_file",
|
|
1036
|
+
path: targetPath,
|
|
1037
|
+
text: text || undefined,
|
|
1038
|
+
content: asString(action.content) || undefined,
|
|
1039
|
+
body: asString(action.body) || undefined,
|
|
1040
|
+
source: source || undefined,
|
|
1041
|
+
filename: asString(action.filename) || asString(action.file_name) || asString(action.name) || asString(action.title) || undefined,
|
|
1042
|
+
append: action.append === true,
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
983
1047
|
if (type === "list_files" || type === "ls") {
|
|
984
1048
|
const filePath = asString(action.path) || "~";
|
|
985
|
-
const limit = typeof action.limit === "number" ? Math.max(1, Math.min(
|
|
1049
|
+
const limit = typeof action.limit === "number" ? Math.max(1, Math.min(5_000, action.limit)) : undefined;
|
|
986
1050
|
actions.push({ type: "list_files", path: filePath, limit });
|
|
987
1051
|
continue;
|
|
988
1052
|
}
|
|
@@ -1192,6 +1256,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1192
1256
|
lastActiveApp = null;
|
|
1193
1257
|
lastVisualTargetDescription = null;
|
|
1194
1258
|
lastVisualTargetApp = null;
|
|
1259
|
+
lastReadFrontmostPage = null;
|
|
1195
1260
|
lastSatisfiedSpotifyDescription = null;
|
|
1196
1261
|
lastSatisfiedSpotifyConfirmedPlaying = false;
|
|
1197
1262
|
lastSatisfiedSpotifyAt = 0;
|
|
@@ -1330,6 +1395,10 @@ export class NativeMacOSJobExecutor {
|
|
|
1330
1395
|
if (action.type === "read_frontmost_page") {
|
|
1331
1396
|
await reporter.progress(progressPercent, `Lendo a pagina ativa em ${action.app || "Safari"}`);
|
|
1332
1397
|
const page = await this.readFrontmostPage(action.app || "Safari");
|
|
1398
|
+
this.lastReadFrontmostPage = {
|
|
1399
|
+
app: action.app || "Safari",
|
|
1400
|
+
...page,
|
|
1401
|
+
};
|
|
1333
1402
|
if (!page.text && this.bridgeConfig?.apiBaseUrl && this.bridgeConfig?.deviceToken) {
|
|
1334
1403
|
await reporter.progress(progressPercent, "Safari bloqueou leitura direta; vou analisar a pagina pela tela");
|
|
1335
1404
|
const screenshotPath = await this.takeScreenshot();
|
|
@@ -1389,14 +1458,39 @@ export class NativeMacOSJobExecutor {
|
|
|
1389
1458
|
}
|
|
1390
1459
|
if (action.type === "read_file") {
|
|
1391
1460
|
await reporter.progress(progressPercent, `Lendo ${action.path}`);
|
|
1392
|
-
const fileContent = await this.
|
|
1393
|
-
|
|
1461
|
+
const fileContent = await this.readLocalFileSnapshot(action.path, action.max_chars);
|
|
1462
|
+
resultPayload.read_file = fileContent;
|
|
1463
|
+
resultPayload.summary = fileContent.summary;
|
|
1464
|
+
completionNotes.push(fileContent.summary);
|
|
1465
|
+
continue;
|
|
1466
|
+
}
|
|
1467
|
+
if (action.type === "trash_path") {
|
|
1468
|
+
await reporter.progress(progressPercent, `Movendo ${action.path} para a Lixeira`);
|
|
1469
|
+
const trashed = await this.movePathToTrashSnapshot(action.path);
|
|
1470
|
+
resultPayload.trash_path = trashed;
|
|
1471
|
+
resultPayload.summary = trashed.summary;
|
|
1472
|
+
completionNotes.push(trashed.summary);
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1475
|
+
if (action.type === "write_text_file") {
|
|
1476
|
+
const targetLabel = action.filename ? `${action.path}/${action.filename}` : action.path;
|
|
1477
|
+
await reporter.progress(progressPercent, `Escrevendo arquivo de texto em ${targetLabel}`);
|
|
1478
|
+
const resolvedContent = this.resolveWriteTextFileContent(action);
|
|
1479
|
+
if (!resolvedContent) {
|
|
1480
|
+
throw new Error("Nenhum texto foi informado para gravar no arquivo local.");
|
|
1481
|
+
}
|
|
1482
|
+
const written = await this.writeTextFileSnapshot(action.path, resolvedContent.content, action.filename, action.append === true, resolvedContent.source);
|
|
1483
|
+
resultPayload.write_text_file = written;
|
|
1484
|
+
resultPayload.summary = written.summary;
|
|
1485
|
+
completionNotes.push(written.summary);
|
|
1394
1486
|
continue;
|
|
1395
1487
|
}
|
|
1396
1488
|
if (action.type === "list_files") {
|
|
1397
1489
|
await reporter.progress(progressPercent, `Listando arquivos em ${action.path}`);
|
|
1398
|
-
const listing = await this.
|
|
1399
|
-
|
|
1490
|
+
const listing = await this.listLocalFilesSnapshot(action.path, action.limit);
|
|
1491
|
+
resultPayload.file_listing = listing;
|
|
1492
|
+
resultPayload.summary = listing.summary;
|
|
1493
|
+
completionNotes.push(listing.summary);
|
|
1400
1494
|
continue;
|
|
1401
1495
|
}
|
|
1402
1496
|
if (action.type === "count_files") {
|
|
@@ -2364,7 +2458,7 @@ return appNames as text
|
|
|
2364
2458
|
snapshot.summary = `A pasta ${targetPath} tem ${entries.length} item${entries.length === 1 ? "" : "s"} e ocupa ${formatBytesCompact(totalSize)}.${childPreview ? ` Itens visiveis agora: ${childPreview}.` : ""}`;
|
|
2365
2459
|
return snapshot;
|
|
2366
2460
|
}
|
|
2367
|
-
const preview = includePreview ? await this.
|
|
2461
|
+
const preview = includePreview ? await this.readLocalFilePreview(resolved, 1200) : undefined;
|
|
2368
2462
|
const snapshot = {
|
|
2369
2463
|
captured_at: new Date().toISOString(),
|
|
2370
2464
|
path: targetPath,
|
|
@@ -4573,25 +4667,68 @@ return {
|
|
|
4573
4667
|
if (targetApp !== "Safari") {
|
|
4574
4668
|
throw new Error("Leitura de pagina frontmost esta disponivel apenas para Safari no momento.");
|
|
4575
4669
|
}
|
|
4576
|
-
const script = `
|
|
4577
|
-
tell application "Safari"
|
|
4578
|
-
activate
|
|
4579
|
-
if (count of windows) = 0 then error "Safari nao possui janelas abertas."
|
|
4580
|
-
delay 1
|
|
4581
|
-
set jsCode to "(function(){const title=document.title||'';const url=location.href||'';const text=((document.body&&document.body.innerText)||'').trim().slice(0,12000);const isYouTubeMusic=location.hostname.includes('music.youtube.com');const isSpotify=location.hostname.includes('open.spotify.com');let playerButton=null;let playerTitle='';let playerState='';if(isYouTubeMusic){playerButton=document.querySelector('ytmusic-player-bar #play-pause-button, ytmusic-player-bar tp-yt-paper-icon-button#play-pause-button, ytmusic-player-bar tp-yt-paper-icon-button.play-pause-button');playerTitle=(Array.from(document.querySelectorAll('ytmusic-player-bar .title, ytmusic-player-bar .content-info-wrapper .title, ytmusic-player-bar [slot=title]')).map((node)=>((node&&node.textContent)||'').trim()).find(Boolean))||'';playerState=(playerButton&&((playerButton.getAttribute('title')||playerButton.getAttribute('aria-label')||playerButton.textContent)||'').trim())||'';}else if(isSpotify){const visible=(node)=>{if(!(node instanceof Element))return false;const rect=node.getBoundingClientRect();if(rect.width<4||rect.height<4)return false;const style=window.getComputedStyle(node);if(style.visibility==='hidden'||style.display==='none'||Number(style.opacity||'1')===0)return false;return rect.bottom>=0&&rect.right>=0&&rect.top<=window.innerHeight&&rect.left<=window.innerWidth;};const spotifyTitleCandidates=Array.from(document.querySelectorAll(\"[data-testid='nowplaying-track-link'], footer a[href*='/track/'], [data-testid='now-playing-widget'] a[href*='/track/'], a[href*='/track/']\")).filter((node)=>visible(node)).map((node)=>({node,text:((node&&node.textContent)||'').trim(),rect:node.getBoundingClientRect()})).filter((entry)=>entry.text).sort((left,right)=>{const leftBottomBias=(left.rect.top>=window.innerHeight*0.72?200:0)+(left.rect.left<=window.innerWidth*0.45?120:0)+left.rect.top;const rightBottomBias=(right.rect.top>=window.innerHeight*0.72?200:0)+(right.rect.left<=window.innerWidth*0.45?120:0)+right.rect.top;return rightBottomBias-leftBottomBias;});playerTitle=(spotifyTitleCandidates[0]&&spotifyTitleCandidates[0].text)||'';playerButton=Array.from(document.querySelectorAll(\"footer button, [data-testid='control-button-playpause'], button[aria-label], button[title]\")).filter((node)=>visible(node)).map((node)=>({node,label:((node.getAttribute('aria-label')||node.getAttribute('title')||node.textContent)||'').trim(),rect:node.getBoundingClientRect()})).filter((entry)=>/play|pause|tocar|pausar|reproduzir/i.test(entry.label)).sort((left,right)=>{const leftScore=(left.rect.top>=window.innerHeight*0.72?200:0)+Math.max(0,200-Math.abs((left.rect.left+left.rect.width/2)-(window.innerWidth/2)));const rightScore=(right.rect.top>=window.innerHeight*0.72?200:0)+Math.max(0,200-Math.abs((right.rect.left+right.rect.width/2)-(window.innerWidth/2)));return rightScore-leftScore;})[0]?.node||null;playerState=(playerButton&&((playerButton.getAttribute('aria-label')||playerButton.getAttribute('title')||playerButton.textContent)||'').trim())||'';}return JSON.stringify({title,url,text,playerTitle,playerState});})();"
|
|
4582
|
-
set pageJson to do JavaScript jsCode in current tab of front window
|
|
4583
|
-
end tell
|
|
4584
|
-
return pageJson
|
|
4585
|
-
`;
|
|
4586
4670
|
try {
|
|
4587
|
-
const
|
|
4588
|
-
|
|
4671
|
+
const page = await this.runSafariJsonScript(`
|
|
4672
|
+
const title = document.title || "";
|
|
4673
|
+
const url = location.href || "";
|
|
4674
|
+
const text = ((document.body && document.body.innerText) || "").trim().slice(0, 12000);
|
|
4675
|
+
const isYouTubeMusic = location.hostname.includes("music.youtube.com");
|
|
4676
|
+
const isSpotify = location.hostname.includes("open.spotify.com");
|
|
4677
|
+
let playerButton = null;
|
|
4678
|
+
let playerTitle = "";
|
|
4679
|
+
let playerState = "";
|
|
4680
|
+
|
|
4681
|
+
if (isYouTubeMusic) {
|
|
4682
|
+
playerButton = document.querySelector("ytmusic-player-bar #play-pause-button, ytmusic-player-bar tp-yt-paper-icon-button#play-pause-button, ytmusic-player-bar tp-yt-paper-icon-button.play-pause-button");
|
|
4683
|
+
playerTitle = (Array.from(document.querySelectorAll("ytmusic-player-bar .title, ytmusic-player-bar .content-info-wrapper .title, ytmusic-player-bar [slot=title]"))
|
|
4684
|
+
.map((node) => ((node && node.textContent) || "").trim())
|
|
4685
|
+
.find(Boolean)) || "";
|
|
4686
|
+
playerState = (playerButton && ((playerButton.getAttribute("title") || playerButton.getAttribute("aria-label") || playerButton.textContent) || "").trim()) || "";
|
|
4687
|
+
} else if (isSpotify) {
|
|
4688
|
+
const visible = (node) => {
|
|
4689
|
+
if (!(node instanceof Element)) return false;
|
|
4690
|
+
const rect = node.getBoundingClientRect();
|
|
4691
|
+
if (rect.width < 4 || rect.height < 4) return false;
|
|
4692
|
+
const style = window.getComputedStyle(node);
|
|
4693
|
+
if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity || "1") === 0) return false;
|
|
4694
|
+
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
|
|
4695
|
+
};
|
|
4696
|
+
const spotifyTitleCandidates = Array.from(document.querySelectorAll("[data-testid='nowplaying-track-link'], footer a[href*='/track/'], [data-testid='now-playing-widget'] a[href*='/track/'], a[href*='/track/']"))
|
|
4697
|
+
.filter((node) => visible(node))
|
|
4698
|
+
.map((node) => ({ node, text: ((node && node.textContent) || "").trim(), rect: node.getBoundingClientRect() }))
|
|
4699
|
+
.filter((entry) => entry.text)
|
|
4700
|
+
.sort((left, right) => {
|
|
4701
|
+
const leftBottomBias = (left.rect.top >= window.innerHeight * 0.72 ? 200 : 0) + (left.rect.left <= window.innerWidth * 0.45 ? 120 : 0) + left.rect.top;
|
|
4702
|
+
const rightBottomBias = (right.rect.top >= window.innerHeight * 0.72 ? 200 : 0) + (right.rect.left <= window.innerWidth * 0.45 ? 120 : 0) + right.rect.top;
|
|
4703
|
+
return rightBottomBias - leftBottomBias;
|
|
4704
|
+
});
|
|
4705
|
+
playerTitle = (spotifyTitleCandidates[0] && spotifyTitleCandidates[0].text) || "";
|
|
4706
|
+
playerButton = Array.from(document.querySelectorAll("footer button, [data-testid='control-button-playpause'], button[aria-label], button[title]"))
|
|
4707
|
+
.filter((node) => visible(node))
|
|
4708
|
+
.map((node) => ({ node, label: ((node.getAttribute("aria-label") || node.getAttribute("title") || node.textContent) || "").trim(), rect: node.getBoundingClientRect() }))
|
|
4709
|
+
.filter((entry) => /play|pause|tocar|pausar|reproduzir/i.test(entry.label))
|
|
4710
|
+
.sort((left, right) => {
|
|
4711
|
+
const leftScore = (left.rect.top >= window.innerHeight * 0.72 ? 200 : 0) + Math.max(0, 200 - Math.abs((left.rect.left + left.rect.width / 2) - (window.innerWidth / 2)));
|
|
4712
|
+
const rightScore = (right.rect.top >= window.innerHeight * 0.72 ? 200 : 0) + Math.max(0, 200 - Math.abs((right.rect.left + right.rect.width / 2) - (window.innerWidth / 2)));
|
|
4713
|
+
return rightScore - leftScore;
|
|
4714
|
+
})[0]?.node || null;
|
|
4715
|
+
playerState = (playerButton && ((playerButton.getAttribute("aria-label") || playerButton.getAttribute("title") || playerButton.textContent) || "").trim()) || "";
|
|
4716
|
+
}
|
|
4717
|
+
|
|
4718
|
+
return {
|
|
4719
|
+
title,
|
|
4720
|
+
url,
|
|
4721
|
+
text,
|
|
4722
|
+
playerTitle,
|
|
4723
|
+
playerState,
|
|
4724
|
+
};
|
|
4725
|
+
`, {}, { activate: true });
|
|
4589
4726
|
return {
|
|
4590
|
-
title:
|
|
4591
|
-
url:
|
|
4592
|
-
text:
|
|
4593
|
-
playerTitle:
|
|
4594
|
-
playerState:
|
|
4727
|
+
title: page.title || "",
|
|
4728
|
+
url: page.url || "",
|
|
4729
|
+
text: page.text || "",
|
|
4730
|
+
playerTitle: page.playerTitle || "",
|
|
4731
|
+
playerState: page.playerState || "",
|
|
4595
4732
|
};
|
|
4596
4733
|
}
|
|
4597
4734
|
catch (error) {
|
|
@@ -4961,9 +5098,9 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
4961
5098
|
resized,
|
|
4962
5099
|
};
|
|
4963
5100
|
}
|
|
4964
|
-
async
|
|
4965
|
-
const resolved = await this.resolveReadableFilePath(filePath);
|
|
5101
|
+
async loadReadableFileContent(resolved) {
|
|
4966
5102
|
const extension = path.extname(resolved).toLowerCase();
|
|
5103
|
+
const mimeType = mimeTypeFromPath(resolved);
|
|
4967
5104
|
if (TEXTUTIL_READABLE_EXTENSIONS.has(extension)) {
|
|
4968
5105
|
const { stdout } = await this.runCommandCapture("textutil", [
|
|
4969
5106
|
"-convert",
|
|
@@ -4971,17 +5108,263 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
4971
5108
|
"-stdout",
|
|
4972
5109
|
resolved,
|
|
4973
5110
|
]);
|
|
4974
|
-
|
|
4975
|
-
|
|
5111
|
+
return {
|
|
5112
|
+
mimeType,
|
|
5113
|
+
encoding: "utf-8",
|
|
5114
|
+
isBinary: false,
|
|
5115
|
+
content: sanitizeTextForJsonTransport(stdout) || "(arquivo sem texto legivel)",
|
|
5116
|
+
};
|
|
4976
5117
|
}
|
|
4977
5118
|
const raw = await readFile(resolved);
|
|
4978
5119
|
if (isLikelyBinaryBuffer(raw)) {
|
|
4979
5120
|
const filename = path.basename(resolved);
|
|
4980
5121
|
const detectedType = extension || "binario";
|
|
4981
|
-
return
|
|
5122
|
+
return {
|
|
5123
|
+
mimeType,
|
|
5124
|
+
isBinary: true,
|
|
5125
|
+
binaryNotice: `O arquivo ${filename} parece ser binario (${detectedType}) e nao pode ser lido como texto puro pelo Otto Bridge ainda.`,
|
|
5126
|
+
};
|
|
5127
|
+
}
|
|
5128
|
+
return {
|
|
5129
|
+
mimeType,
|
|
5130
|
+
encoding: "utf-8",
|
|
5131
|
+
isBinary: false,
|
|
5132
|
+
content: sanitizeTextForJsonTransport(raw.toString("utf8")) || "(arquivo vazio)",
|
|
5133
|
+
};
|
|
5134
|
+
}
|
|
5135
|
+
async readLocalFilePreview(filePath, maxChars = 1200) {
|
|
5136
|
+
const resolved = await this.resolveReadableFilePath(filePath);
|
|
5137
|
+
const loaded = await this.loadReadableFileContent(resolved);
|
|
5138
|
+
if (loaded.isBinary) {
|
|
5139
|
+
return clipText(loaded.binaryNotice || "O arquivo parece ser binario e nao pode ser lido como texto puro.", maxChars);
|
|
5140
|
+
}
|
|
5141
|
+
return clipTextPreview(loaded.content || "(arquivo vazio)", maxChars);
|
|
5142
|
+
}
|
|
5143
|
+
async readLocalFileSnapshot(filePath, chunkSizeChars = 4000) {
|
|
5144
|
+
const resolved = await this.resolveReadableFilePath(filePath);
|
|
5145
|
+
const entryStat = await stat(resolved);
|
|
5146
|
+
const loaded = await this.loadReadableFileContent(resolved);
|
|
5147
|
+
const fileName = path.basename(resolved) || resolved;
|
|
5148
|
+
if (loaded.isBinary) {
|
|
5149
|
+
return {
|
|
5150
|
+
captured_at: new Date().toISOString(),
|
|
5151
|
+
path: filePath,
|
|
5152
|
+
resolved_path: resolved,
|
|
5153
|
+
name: fileName,
|
|
5154
|
+
mime_type: loaded.mimeType,
|
|
5155
|
+
size_bytes: entryStat.size,
|
|
5156
|
+
modified_at: entryStat.mtime.toISOString(),
|
|
5157
|
+
is_binary: true,
|
|
5158
|
+
binary_notice: loaded.binaryNotice,
|
|
5159
|
+
summary: loaded.binaryNotice || `O arquivo ${filePath} parece ser binario e nao pode ser lido como texto puro.`,
|
|
5160
|
+
};
|
|
5161
|
+
}
|
|
5162
|
+
const content = loaded.content || "(arquivo vazio)";
|
|
5163
|
+
const chunks = chunkTextForTransport(content, chunkSizeChars);
|
|
5164
|
+
return {
|
|
5165
|
+
captured_at: new Date().toISOString(),
|
|
5166
|
+
path: filePath,
|
|
5167
|
+
resolved_path: resolved,
|
|
5168
|
+
name: fileName,
|
|
5169
|
+
mime_type: loaded.mimeType,
|
|
5170
|
+
size_bytes: entryStat.size,
|
|
5171
|
+
modified_at: entryStat.mtime.toISOString(),
|
|
5172
|
+
is_binary: false,
|
|
5173
|
+
encoding: loaded.encoding || "utf-8",
|
|
5174
|
+
content: chunks.length <= 1 ? content : undefined,
|
|
5175
|
+
content_char_count: content.length,
|
|
5176
|
+
chunk_size_chars: Math.max(400, Math.min(Math.round(chunkSizeChars || 4000), 32_000)),
|
|
5177
|
+
chunk_count: chunks.length,
|
|
5178
|
+
chunks,
|
|
5179
|
+
summary: `Li ${filePath} por completo (${content.length} caracteres em ${chunks.length} bloco${chunks.length === 1 ? "" : "s"}).`,
|
|
5180
|
+
};
|
|
5181
|
+
}
|
|
5182
|
+
async resolveTrashTargetPath(targetPath) {
|
|
5183
|
+
const resolved = expandUserPath(targetPath);
|
|
5184
|
+
try {
|
|
5185
|
+
await stat(resolved);
|
|
5186
|
+
return resolved;
|
|
5187
|
+
}
|
|
5188
|
+
catch {
|
|
5189
|
+
// Continue into heuristic search below.
|
|
5190
|
+
}
|
|
5191
|
+
const targetName = path.basename(resolved).trim();
|
|
5192
|
+
if (!targetName || targetName === "." || targetName === path.sep) {
|
|
5193
|
+
return resolved;
|
|
4982
5194
|
}
|
|
4983
|
-
const
|
|
4984
|
-
|
|
5195
|
+
const homeDir = os.homedir();
|
|
5196
|
+
const requestedDir = path.dirname(resolved);
|
|
5197
|
+
const preferredRoots = uniqueStrings([
|
|
5198
|
+
requestedDir && requestedDir !== homeDir ? requestedDir : null,
|
|
5199
|
+
path.join(homeDir, "Downloads"),
|
|
5200
|
+
path.join(homeDir, "Desktop"),
|
|
5201
|
+
path.join(homeDir, "Documents"),
|
|
5202
|
+
homeDir,
|
|
5203
|
+
]);
|
|
5204
|
+
const found = await this.findPathByName(targetName, preferredRoots);
|
|
5205
|
+
return found || resolved;
|
|
5206
|
+
}
|
|
5207
|
+
async findPathByName(targetName, roots) {
|
|
5208
|
+
const normalizedTarget = normalizeText(targetName).replace(/\s+/g, " ").trim();
|
|
5209
|
+
if (!normalizedTarget) {
|
|
5210
|
+
return null;
|
|
5211
|
+
}
|
|
5212
|
+
for (const root of roots) {
|
|
5213
|
+
let rootStat;
|
|
5214
|
+
try {
|
|
5215
|
+
rootStat = await stat(root);
|
|
5216
|
+
}
|
|
5217
|
+
catch {
|
|
5218
|
+
continue;
|
|
5219
|
+
}
|
|
5220
|
+
if (!rootStat.isDirectory()) {
|
|
5221
|
+
continue;
|
|
5222
|
+
}
|
|
5223
|
+
const queue = [root];
|
|
5224
|
+
while (queue.length > 0) {
|
|
5225
|
+
const current = queue.shift();
|
|
5226
|
+
if (!current)
|
|
5227
|
+
continue;
|
|
5228
|
+
let entries;
|
|
5229
|
+
try {
|
|
5230
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
5231
|
+
}
|
|
5232
|
+
catch {
|
|
5233
|
+
continue;
|
|
5234
|
+
}
|
|
5235
|
+
for (const entry of entries) {
|
|
5236
|
+
const entryPath = path.join(current, entry.name);
|
|
5237
|
+
const normalizedEntryName = normalizeText(entry.name).replace(/\s+/g, " ").trim();
|
|
5238
|
+
if (normalizedEntryName === normalizedTarget) {
|
|
5239
|
+
return entryPath;
|
|
5240
|
+
}
|
|
5241
|
+
if (entry.isDirectory()) {
|
|
5242
|
+
if (!FILE_SEARCH_SKIP_DIRS.has(entry.name)) {
|
|
5243
|
+
queue.push(entryPath);
|
|
5244
|
+
}
|
|
5245
|
+
continue;
|
|
5246
|
+
}
|
|
5247
|
+
if (entry.isFile() && entry.name.toLowerCase() === targetName.toLowerCase()) {
|
|
5248
|
+
return entryPath;
|
|
5249
|
+
}
|
|
5250
|
+
}
|
|
5251
|
+
}
|
|
5252
|
+
}
|
|
5253
|
+
return null;
|
|
5254
|
+
}
|
|
5255
|
+
async resolveUniqueTrashDestination(targetPath) {
|
|
5256
|
+
const ext = path.extname(targetPath);
|
|
5257
|
+
const stem = ext ? targetPath.slice(0, -ext.length) : targetPath;
|
|
5258
|
+
let attempt = 0;
|
|
5259
|
+
let candidate = targetPath;
|
|
5260
|
+
while (true) {
|
|
5261
|
+
try {
|
|
5262
|
+
await stat(candidate);
|
|
5263
|
+
attempt += 1;
|
|
5264
|
+
candidate = `${stem} ${attempt + 1}${ext}`;
|
|
5265
|
+
}
|
|
5266
|
+
catch {
|
|
5267
|
+
return candidate;
|
|
5268
|
+
}
|
|
5269
|
+
}
|
|
5270
|
+
}
|
|
5271
|
+
async movePathToTrashSnapshot(targetPath) {
|
|
5272
|
+
const resolved = await this.resolveTrashTargetPath(targetPath);
|
|
5273
|
+
const entryStat = await stat(resolved);
|
|
5274
|
+
const trashDir = path.join(os.homedir(), ".Trash");
|
|
5275
|
+
await mkdir(trashDir, { recursive: true });
|
|
5276
|
+
const name = path.basename(resolved) || resolved;
|
|
5277
|
+
const trashedPath = await this.resolveUniqueTrashDestination(path.join(trashDir, name));
|
|
5278
|
+
await rename(resolved, trashedPath);
|
|
5279
|
+
const kind = entryStat.isDirectory()
|
|
5280
|
+
? "directory"
|
|
5281
|
+
: entryStat.isFile()
|
|
5282
|
+
? "file"
|
|
5283
|
+
: "other";
|
|
5284
|
+
return {
|
|
5285
|
+
captured_at: new Date().toISOString(),
|
|
5286
|
+
path: targetPath,
|
|
5287
|
+
resolved_path: resolved,
|
|
5288
|
+
trashed_path: trashedPath,
|
|
5289
|
+
name,
|
|
5290
|
+
kind,
|
|
5291
|
+
size_bytes: entryStat.size,
|
|
5292
|
+
modified_at: entryStat.mtime.toISOString(),
|
|
5293
|
+
summary: `${kind === "directory" ? "Mandei a pasta" : kind === "file" ? "Mandei o arquivo" : "Mandei o item"} ${name} para a Lixeira.`,
|
|
5294
|
+
};
|
|
5295
|
+
}
|
|
5296
|
+
async resolveWritableTextFilePath(targetPath, filename) {
|
|
5297
|
+
const expanded = expandUserPath(targetPath);
|
|
5298
|
+
const requestedFilename = filename ? sanitizeFileName(filename) : null;
|
|
5299
|
+
if (requestedFilename) {
|
|
5300
|
+
try {
|
|
5301
|
+
const existingStat = await stat(expanded);
|
|
5302
|
+
if (existingStat.isDirectory()) {
|
|
5303
|
+
return path.join(expanded, requestedFilename);
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
5306
|
+
catch {
|
|
5307
|
+
// Continue below and treat the target as a direct file path.
|
|
5308
|
+
}
|
|
5309
|
+
if (String(targetPath || "").trimEnd().endsWith(path.sep)) {
|
|
5310
|
+
return path.join(expanded, requestedFilename);
|
|
5311
|
+
}
|
|
5312
|
+
}
|
|
5313
|
+
try {
|
|
5314
|
+
const existingStat = await stat(expanded);
|
|
5315
|
+
if (existingStat.isDirectory()) {
|
|
5316
|
+
return path.join(expanded, sanitizeFileName("otto-note.txt"));
|
|
5317
|
+
}
|
|
5318
|
+
}
|
|
5319
|
+
catch {
|
|
5320
|
+
// Continue below and treat the target as a direct file path.
|
|
5321
|
+
}
|
|
5322
|
+
if (String(targetPath || "").trimEnd().endsWith(path.sep)) {
|
|
5323
|
+
return path.join(expanded, sanitizeFileName("otto-note.txt"));
|
|
5324
|
+
}
|
|
5325
|
+
return expanded;
|
|
5326
|
+
}
|
|
5327
|
+
async writeTextFileSnapshot(targetPath, text, filename, append = false, source) {
|
|
5328
|
+
const resolved = await this.resolveWritableTextFilePath(targetPath, filename);
|
|
5329
|
+
const parentDir = path.dirname(resolved);
|
|
5330
|
+
await mkdir(parentDir, { recursive: true });
|
|
5331
|
+
await writeFile(resolved, text, {
|
|
5332
|
+
encoding: "utf8",
|
|
5333
|
+
flag: append ? "a" : "w",
|
|
5334
|
+
});
|
|
5335
|
+
const entryStat = await stat(resolved);
|
|
5336
|
+
const name = path.basename(resolved) || resolved;
|
|
5337
|
+
const preview = clipTextPreview(String(text || "") || "(arquivo vazio)", 240);
|
|
5338
|
+
return {
|
|
5339
|
+
captured_at: new Date().toISOString(),
|
|
5340
|
+
path: targetPath,
|
|
5341
|
+
resolved_path: resolved,
|
|
5342
|
+
name,
|
|
5343
|
+
mime_type: "text/plain; charset=utf-8",
|
|
5344
|
+
size_bytes: entryStat.size,
|
|
5345
|
+
modified_at: entryStat.mtime.toISOString(),
|
|
5346
|
+
append,
|
|
5347
|
+
content_char_count: String(text || "").length,
|
|
5348
|
+
content_preview: preview,
|
|
5349
|
+
source: source || undefined,
|
|
5350
|
+
summary: `${append ? "Atualizei" : "Escrevi"} ${String(text || "").length} caractere${String(text || "").length === 1 ? "" : "s"} em ${resolved}.`,
|
|
5351
|
+
};
|
|
5352
|
+
}
|
|
5353
|
+
resolveWriteTextFileContent(action) {
|
|
5354
|
+
const explicitText = [action.text, action.content, action.body]
|
|
5355
|
+
.map((value) => String(value || "").trim())
|
|
5356
|
+
.find(Boolean);
|
|
5357
|
+
if (explicitText) {
|
|
5358
|
+
return { content: explicitText };
|
|
5359
|
+
}
|
|
5360
|
+
const source = String(action.source || "").trim().toLowerCase();
|
|
5361
|
+
if (source === "last_page_text" || source === "last_page_summary") {
|
|
5362
|
+
const pageText = String(this.lastReadFrontmostPage?.text || "").trim();
|
|
5363
|
+
if (pageText) {
|
|
5364
|
+
return { content: pageText, source };
|
|
5365
|
+
}
|
|
5366
|
+
}
|
|
5367
|
+
return null;
|
|
4985
5368
|
}
|
|
4986
5369
|
async resolveReadableFilePath(filePath) {
|
|
4987
5370
|
const resolved = expandUserPath(filePath);
|
|
@@ -5052,22 +5435,55 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
5052
5435
|
}
|
|
5053
5436
|
return null;
|
|
5054
5437
|
}
|
|
5055
|
-
async
|
|
5438
|
+
async listLocalFilesSnapshot(directoryPath, limit) {
|
|
5056
5439
|
const resolved = expandUserPath(directoryPath);
|
|
5057
|
-
const
|
|
5058
|
-
const
|
|
5440
|
+
const allEntries = await readdir(resolved, { withFileTypes: true });
|
|
5441
|
+
const sortedEntries = allEntries.sort((left, right) => {
|
|
5442
|
+
if (left.isDirectory() !== right.isDirectory()) {
|
|
5443
|
+
return left.isDirectory() ? -1 : 1;
|
|
5444
|
+
}
|
|
5445
|
+
return left.name.localeCompare(right.name);
|
|
5446
|
+
});
|
|
5447
|
+
const effectiveLimit = typeof limit === "number" && Number.isFinite(limit) ? Math.max(1, Math.min(Math.round(limit), 5_000)) : null;
|
|
5448
|
+
const selectedEntries = effectiveLimit ? sortedEntries.slice(0, effectiveLimit) : sortedEntries;
|
|
5449
|
+
const items = await Promise.all(selectedEntries.map(async (entry) => {
|
|
5059
5450
|
const entryPath = path.join(resolved, entry.name);
|
|
5060
|
-
let
|
|
5451
|
+
let sizeBytes;
|
|
5452
|
+
let modifiedAt;
|
|
5061
5453
|
try {
|
|
5062
5454
|
const entryStat = await stat(entryPath);
|
|
5063
|
-
|
|
5455
|
+
if (!entry.isDirectory()) {
|
|
5456
|
+
sizeBytes = entryStat.size;
|
|
5457
|
+
}
|
|
5458
|
+
modifiedAt = entryStat.mtime.toISOString();
|
|
5064
5459
|
}
|
|
5065
5460
|
catch {
|
|
5066
|
-
|
|
5461
|
+
// Ignore stat failures and return the visible entry metadata we have.
|
|
5067
5462
|
}
|
|
5068
|
-
return
|
|
5463
|
+
return {
|
|
5464
|
+
name: entry.name,
|
|
5465
|
+
path: entryPath,
|
|
5466
|
+
kind: entry.isDirectory() ? "directory" : (entry.isFile() ? "file" : "other"),
|
|
5467
|
+
size_bytes: sizeBytes,
|
|
5468
|
+
modified_at: modifiedAt,
|
|
5469
|
+
};
|
|
5069
5470
|
}));
|
|
5070
|
-
|
|
5471
|
+
const summary = items.length === 0
|
|
5472
|
+
? `A pasta ${directoryPath} esta vazia.`
|
|
5473
|
+
: effectiveLimit && allEntries.length > items.length
|
|
5474
|
+
? `Listei ${items.length} itens visiveis em ${directoryPath} agora. A pasta tem ${allEntries.length} itens no total.`
|
|
5475
|
+
: `Listei ${items.length} itens de ${directoryPath} por completo.`;
|
|
5476
|
+
return {
|
|
5477
|
+
captured_at: new Date().toISOString(),
|
|
5478
|
+
path: directoryPath,
|
|
5479
|
+
resolved_path: resolved,
|
|
5480
|
+
name: path.basename(resolved) || resolved,
|
|
5481
|
+
item_count: items.length,
|
|
5482
|
+
total_item_count: allEntries.length,
|
|
5483
|
+
limit_applied: effectiveLimit || undefined,
|
|
5484
|
+
entries: items,
|
|
5485
|
+
summary,
|
|
5486
|
+
};
|
|
5071
5487
|
}
|
|
5072
5488
|
async countLocalFiles(directoryPath, extensions, recursive = true) {
|
|
5073
5489
|
const resolved = expandUserPath(directoryPath);
|
|
@@ -5410,6 +5826,12 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
5410
5826
|
if (action.type === "read_file") {
|
|
5411
5827
|
return `${action.path} foi lido no macOS`;
|
|
5412
5828
|
}
|
|
5829
|
+
if (action.type === "trash_path") {
|
|
5830
|
+
return `${action.path} foi movido para a Lixeira`;
|
|
5831
|
+
}
|
|
5832
|
+
if (action.type === "write_text_file") {
|
|
5833
|
+
return `Arquivo de texto escrito em ${action.filename ? `${action.path}/${action.filename}` : action.path}`;
|
|
5834
|
+
}
|
|
5413
5835
|
if (action.type === "list_files") {
|
|
5414
5836
|
return `Arquivos listados em ${action.path}`;
|
|
5415
5837
|
}
|
package/dist/tool_catalog.js
CHANGED
|
@@ -65,6 +65,23 @@ const NATIVE_MACOS_BASE_TOOLS = [
|
|
|
65
65
|
mode: "observe",
|
|
66
66
|
action_types: ["read_file"],
|
|
67
67
|
},
|
|
68
|
+
{
|
|
69
|
+
id: "filesystem.write",
|
|
70
|
+
title: "Escrita de arquivo de texto",
|
|
71
|
+
description: "Cria ou atualiza arquivos .txt em caminhos locais, incluindo Desktop e outras pastas do usuario.",
|
|
72
|
+
category: "filesystem",
|
|
73
|
+
mode: "write",
|
|
74
|
+
action_types: ["write_text_file"],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "filesystem.trash",
|
|
78
|
+
title: "Mover item para a Lixeira",
|
|
79
|
+
description: "Move arquivos e pastas locais para a Lixeira do macOS com confirmação.",
|
|
80
|
+
category: "filesystem",
|
|
81
|
+
mode: "write",
|
|
82
|
+
action_types: ["trash_path"],
|
|
83
|
+
requires_confirmation: true,
|
|
84
|
+
},
|
|
68
85
|
{
|
|
69
86
|
id: "system.health",
|
|
70
87
|
title: "Status do Mac",
|
package/dist/types.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const BRIDGE_CONFIG_VERSION = 1;
|
|
2
|
-
export const BRIDGE_VERSION = "0.9.
|
|
2
|
+
export const BRIDGE_VERSION = "0.9.2";
|
|
3
3
|
export const BRIDGE_PACKAGE_NAME = "@leg3ndy/otto-bridge";
|
|
4
4
|
export const DEFAULT_API_BASE_URL = "http://localhost:8000";
|
|
5
5
|
export const DEFAULT_POLL_INTERVAL_MS = 3000;
|
|
@@ -10,4 +10,4 @@ export const DEFAULT_RECONNECT_MAX_DELAY_MS = 15000;
|
|
|
10
10
|
export const DEFAULT_EXECUTOR_TYPE = "mock";
|
|
11
11
|
export const DEFAULT_CLAWD_CURSOR_BASE_URL = "http://127.0.0.1:3847";
|
|
12
12
|
export const DEFAULT_CLAWD_CURSOR_POLL_INTERVAL_MS = 1500;
|
|
13
|
-
export const BRIDGE_LOCAL_TOOLS_VERSION =
|
|
13
|
+
export const BRIDGE_LOCAL_TOOLS_VERSION = 2;
|