@leg3ndy/otto-bridge 0.9.1 → 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 +8 -8
- package/dist/executors/native_macos.js +315 -18
- package/dist/tool_catalog.js +17 -0
- package/dist/types.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ 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.
|
|
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
19
|
|
|
20
20
|
## Distribuicao
|
|
21
21
|
|
|
@@ -37,14 +37,14 @@ Enquanto o pacote nao estiver publicado, voce pode gerar um tarball local:
|
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
39
|
npm pack
|
|
40
|
-
npm install -g ./leg3ndy-otto-bridge-0.9.
|
|
40
|
+
npm install -g ./leg3ndy-otto-bridge-0.9.2.tgz
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
Na linha `0.9.
|
|
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.
|
|
44
44
|
|
|
45
|
-
No macOS, a linha `0.9.
|
|
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`.
|
|
46
46
|
|
|
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.
|
|
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.
|
|
48
48
|
|
|
49
49
|
## Publicacao
|
|
50
50
|
|
|
@@ -114,7 +114,7 @@ otto-bridge run --executor clawd-cursor --clawd-url http://127.0.0.1:3847
|
|
|
114
114
|
|
|
115
115
|
### WhatsApp Web em background
|
|
116
116
|
|
|
117
|
-
Fluxo recomendado na linha `0.9.
|
|
117
|
+
Fluxo recomendado na linha `0.9.2`:
|
|
118
118
|
|
|
119
119
|
```bash
|
|
120
120
|
otto-bridge extensions --install whatsappweb
|
|
@@ -124,13 +124,13 @@ otto-bridge extensions --status whatsappweb
|
|
|
124
124
|
|
|
125
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.
|
|
126
126
|
|
|
127
|
-
Contrato da linha `0.9.
|
|
127
|
+
Contrato da linha `0.9.2`:
|
|
128
128
|
|
|
129
129
|
- `otto-bridge extensions --setup whatsappweb`: autentica a sessao uma vez
|
|
130
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
|
|
131
131
|
- ao parar o `otto-bridge run`: o browser em background e desligado, mas a sessao local fica lembrada para o proximo boot
|
|
132
132
|
|
|
133
|
-
## Handoff rapido da linha 0.9.
|
|
133
|
+
## Handoff rapido da linha 0.9.2
|
|
134
134
|
|
|
135
135
|
Ja fechado no codigo:
|
|
136
136
|
|
|
@@ -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,17 @@ 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
|
+
}
|
|
705
716
|
function chunkTextForTransport(value, chunkSize) {
|
|
706
717
|
const normalized = String(value || "");
|
|
707
718
|
if (!normalized) {
|
|
@@ -1009,6 +1020,30 @@ function parseStructuredActions(job) {
|
|
|
1009
1020
|
}
|
|
1010
1021
|
continue;
|
|
1011
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
|
+
}
|
|
1012
1047
|
if (type === "list_files" || type === "ls") {
|
|
1013
1048
|
const filePath = asString(action.path) || "~";
|
|
1014
1049
|
const limit = typeof action.limit === "number" ? Math.max(1, Math.min(5_000, action.limit)) : undefined;
|
|
@@ -1221,6 +1256,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1221
1256
|
lastActiveApp = null;
|
|
1222
1257
|
lastVisualTargetDescription = null;
|
|
1223
1258
|
lastVisualTargetApp = null;
|
|
1259
|
+
lastReadFrontmostPage = null;
|
|
1224
1260
|
lastSatisfiedSpotifyDescription = null;
|
|
1225
1261
|
lastSatisfiedSpotifyConfirmedPlaying = false;
|
|
1226
1262
|
lastSatisfiedSpotifyAt = 0;
|
|
@@ -1359,6 +1395,10 @@ export class NativeMacOSJobExecutor {
|
|
|
1359
1395
|
if (action.type === "read_frontmost_page") {
|
|
1360
1396
|
await reporter.progress(progressPercent, `Lendo a pagina ativa em ${action.app || "Safari"}`);
|
|
1361
1397
|
const page = await this.readFrontmostPage(action.app || "Safari");
|
|
1398
|
+
this.lastReadFrontmostPage = {
|
|
1399
|
+
app: action.app || "Safari",
|
|
1400
|
+
...page,
|
|
1401
|
+
};
|
|
1362
1402
|
if (!page.text && this.bridgeConfig?.apiBaseUrl && this.bridgeConfig?.deviceToken) {
|
|
1363
1403
|
await reporter.progress(progressPercent, "Safari bloqueou leitura direta; vou analisar a pagina pela tela");
|
|
1364
1404
|
const screenshotPath = await this.takeScreenshot();
|
|
@@ -1424,6 +1464,27 @@ export class NativeMacOSJobExecutor {
|
|
|
1424
1464
|
completionNotes.push(fileContent.summary);
|
|
1425
1465
|
continue;
|
|
1426
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);
|
|
1486
|
+
continue;
|
|
1487
|
+
}
|
|
1427
1488
|
if (action.type === "list_files") {
|
|
1428
1489
|
await reporter.progress(progressPercent, `Listando arquivos em ${action.path}`);
|
|
1429
1490
|
const listing = await this.listLocalFilesSnapshot(action.path, action.limit);
|
|
@@ -4606,25 +4667,68 @@ return {
|
|
|
4606
4667
|
if (targetApp !== "Safari") {
|
|
4607
4668
|
throw new Error("Leitura de pagina frontmost esta disponivel apenas para Safari no momento.");
|
|
4608
4669
|
}
|
|
4609
|
-
const script = `
|
|
4610
|
-
tell application "Safari"
|
|
4611
|
-
activate
|
|
4612
|
-
if (count of windows) = 0 then error "Safari nao possui janelas abertas."
|
|
4613
|
-
delay 1
|
|
4614
|
-
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});})();"
|
|
4615
|
-
set pageJson to do JavaScript jsCode in current tab of front window
|
|
4616
|
-
end tell
|
|
4617
|
-
return pageJson
|
|
4618
|
-
`;
|
|
4619
4670
|
try {
|
|
4620
|
-
const
|
|
4621
|
-
|
|
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 });
|
|
4622
4726
|
return {
|
|
4623
|
-
title:
|
|
4624
|
-
url:
|
|
4625
|
-
text:
|
|
4626
|
-
playerTitle:
|
|
4627
|
-
playerState:
|
|
4727
|
+
title: page.title || "",
|
|
4728
|
+
url: page.url || "",
|
|
4729
|
+
text: page.text || "",
|
|
4730
|
+
playerTitle: page.playerTitle || "",
|
|
4731
|
+
playerState: page.playerState || "",
|
|
4628
4732
|
};
|
|
4629
4733
|
}
|
|
4630
4734
|
catch (error) {
|
|
@@ -5075,6 +5179,193 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
5075
5179
|
summary: `Li ${filePath} por completo (${content.length} caracteres em ${chunks.length} bloco${chunks.length === 1 ? "" : "s"}).`,
|
|
5076
5180
|
};
|
|
5077
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;
|
|
5194
|
+
}
|
|
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;
|
|
5368
|
+
}
|
|
5078
5369
|
async resolveReadableFilePath(filePath) {
|
|
5079
5370
|
const resolved = expandUserPath(filePath);
|
|
5080
5371
|
try {
|
|
@@ -5535,6 +5826,12 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
5535
5826
|
if (action.type === "read_file") {
|
|
5536
5827
|
return `${action.path} foi lido no macOS`;
|
|
5537
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
|
+
}
|
|
5538
5835
|
if (action.type === "list_files") {
|
|
5539
5836
|
return `Arquivos listados em ${action.path}`;
|
|
5540
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;
|