@leg3ndy/otto-bridge 0.8.3 → 0.9.0
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 -6
- package/dist/executors/native_macos.js +757 -0
- package/dist/runtime.js +14 -0
- package/dist/tool_catalog.js +193 -0
- package/dist/types.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,6 +13,8 @@ Para um passo a passo de instalacao, pareamento, uso, desconexao e desinstalacao
|
|
|
13
13
|
|
|
14
14
|
Para o estado atual da arquitetura, capacidades entregues, limitacoes e roadmap do Otto Bridge, veja [`leg3ndy-ai-backend/docs/OTTO_BRIDGE_ARCHITECTURE.md`](../leg3ndy-ai-backend/docs/OTTO_BRIDGE_ARCHITECTURE.md).
|
|
15
15
|
|
|
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
|
+
|
|
16
18
|
## Distribuicao
|
|
17
19
|
|
|
18
20
|
Fluxo recomendado agora:
|
|
@@ -33,12 +35,14 @@ Enquanto o pacote nao estiver publicado, voce pode gerar um tarball local:
|
|
|
33
35
|
|
|
34
36
|
```bash
|
|
35
37
|
npm pack
|
|
36
|
-
npm install -g ./leg3ndy-otto-bridge-0.
|
|
38
|
+
npm install -g ./leg3ndy-otto-bridge-0.9.0.tgz
|
|
37
39
|
```
|
|
38
40
|
|
|
39
|
-
No `0.
|
|
41
|
+
No `0.9.0`, `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
|
+
|
|
43
|
+
No macOS, o `0.9.0` 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`.
|
|
40
44
|
|
|
41
|
-
No
|
|
45
|
+
No nivel arquitetural, o `0.9.0` muda o papel do bridge: ele publica tools e resultados estruturados para o Otto, em vez de injetar resposta pronta como caminho principal do chat.
|
|
42
46
|
|
|
43
47
|
## Publicacao
|
|
44
48
|
|
|
@@ -108,7 +112,7 @@ otto-bridge run --executor clawd-cursor --clawd-url http://127.0.0.1:3847
|
|
|
108
112
|
|
|
109
113
|
### WhatsApp Web em background
|
|
110
114
|
|
|
111
|
-
Fluxo recomendado no `0.
|
|
115
|
+
Fluxo recomendado no `0.9.0`:
|
|
112
116
|
|
|
113
117
|
```bash
|
|
114
118
|
otto-bridge extensions --install whatsappweb
|
|
@@ -118,13 +122,13 @@ otto-bridge extensions --status whatsappweb
|
|
|
118
122
|
|
|
119
123
|
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.
|
|
120
124
|
|
|
121
|
-
Contrato do `0.
|
|
125
|
+
Contrato do `0.9.0`:
|
|
122
126
|
|
|
123
127
|
- `otto-bridge extensions --setup whatsappweb`: autentica a sessao uma vez
|
|
124
128
|
- `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
|
|
125
129
|
- ao parar o `otto-bridge run`: o browser em background e desligado, mas a sessao local fica lembrada para o proximo boot
|
|
126
130
|
|
|
127
|
-
## Handoff rapido do 0.
|
|
131
|
+
## Handoff rapido do 0.9.0
|
|
128
132
|
|
|
129
133
|
Ja fechado no codigo:
|
|
130
134
|
|
|
@@ -132,6 +136,8 @@ Ja fechado no codigo:
|
|
|
132
136
|
- user-agent do helper ajustado para evitar bloqueio do WhatsApp por detecao de Safari/WebKit
|
|
133
137
|
- resultado final dos `device_job` agora e persistido como contexto mais forte para o proximo turno do Otto
|
|
134
138
|
- prompt bridge-aware no chat normal para ajudar o Otto a responder com base no que realmente aconteceu no device
|
|
139
|
+
- runtime local agora publica `local_tools` para o Otto/backend saberem exatamente o que o device consegue fazer
|
|
140
|
+
- o caminho principal do bridge usa `summary`/`narration_context` no lugar de resposta automatica pronta
|
|
135
141
|
|
|
136
142
|
Ainda precisa reteste em campo:
|
|
137
143
|
|
|
@@ -778,6 +778,46 @@ function noteBodyToHtml(text) {
|
|
|
778
778
|
.map((block) => `<p>${escapeHtml(block).replace(/\n/g, "<br>")}</p>`);
|
|
779
779
|
return blocks.join("");
|
|
780
780
|
}
|
|
781
|
+
function formatBytesCompact(bytes) {
|
|
782
|
+
if (!Number.isFinite(bytes) || !bytes || bytes <= 0) {
|
|
783
|
+
return "0 B";
|
|
784
|
+
}
|
|
785
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
786
|
+
let value = Number(bytes);
|
|
787
|
+
let unitIndex = 0;
|
|
788
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
789
|
+
value /= 1024;
|
|
790
|
+
unitIndex += 1;
|
|
791
|
+
}
|
|
792
|
+
const fractionDigits = value >= 10 || unitIndex === 0 ? 0 : 1;
|
|
793
|
+
return `${value.toFixed(fractionDigits)} ${units[unitIndex]}`;
|
|
794
|
+
}
|
|
795
|
+
function roundMetric(value, decimals = 0) {
|
|
796
|
+
if (!Number.isFinite(value)) {
|
|
797
|
+
return 0;
|
|
798
|
+
}
|
|
799
|
+
const factor = 10 ** decimals;
|
|
800
|
+
return Math.round(value * factor) / factor;
|
|
801
|
+
}
|
|
802
|
+
function parseScaledBytes(rawValue, rawUnit) {
|
|
803
|
+
const value = Number(rawValue);
|
|
804
|
+
if (!Number.isFinite(value)) {
|
|
805
|
+
return 0;
|
|
806
|
+
}
|
|
807
|
+
const normalizedUnit = rawUnit.trim().toUpperCase();
|
|
808
|
+
const multipliers = {
|
|
809
|
+
B: 1,
|
|
810
|
+
K: 1024,
|
|
811
|
+
KB: 1024,
|
|
812
|
+
M: 1024 ** 2,
|
|
813
|
+
MB: 1024 ** 2,
|
|
814
|
+
G: 1024 ** 3,
|
|
815
|
+
GB: 1024 ** 3,
|
|
816
|
+
T: 1024 ** 4,
|
|
817
|
+
TB: 1024 ** 4,
|
|
818
|
+
};
|
|
819
|
+
return Math.round(value * (multipliers[normalizedUnit] || 1));
|
|
820
|
+
}
|
|
781
821
|
function isSafeShellCommand(command) {
|
|
782
822
|
const trimmed = command.trim();
|
|
783
823
|
if (!trimmed) {
|
|
@@ -899,6 +939,39 @@ function parseStructuredActions(job) {
|
|
|
899
939
|
});
|
|
900
940
|
continue;
|
|
901
941
|
}
|
|
942
|
+
if (type === "browser_context") {
|
|
943
|
+
actions.push({
|
|
944
|
+
type: "browser_context",
|
|
945
|
+
app: asString(action.app) || asString(action.application) || undefined,
|
|
946
|
+
include_text: action.include_text === true,
|
|
947
|
+
include_tabs: action.include_tabs === true,
|
|
948
|
+
});
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
if (type === "app_status") {
|
|
952
|
+
actions.push({
|
|
953
|
+
type: "app_status",
|
|
954
|
+
app: asString(action.app) || asString(action.application) || undefined,
|
|
955
|
+
include_frontmost: action.include_frontmost === true,
|
|
956
|
+
include_running_apps: action.include_running_apps === true,
|
|
957
|
+
include_top_processes: action.include_top_processes === true,
|
|
958
|
+
});
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
if (type === "filesystem_inspect") {
|
|
962
|
+
const targetPath = asString(action.path);
|
|
963
|
+
if (targetPath) {
|
|
964
|
+
const rawLimit = Number(action.limit);
|
|
965
|
+
actions.push({
|
|
966
|
+
type: "filesystem_inspect",
|
|
967
|
+
path: targetPath,
|
|
968
|
+
include_children: action.include_children === true,
|
|
969
|
+
include_preview: action.include_preview === true,
|
|
970
|
+
limit: Number.isFinite(rawLimit) ? Math.max(1, Math.min(Math.round(rawLimit), 20)) : undefined,
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
902
975
|
if (type === "read_file" || type === "read_local_file") {
|
|
903
976
|
const filePath = asString(action.path);
|
|
904
977
|
if (filePath) {
|
|
@@ -922,6 +995,19 @@ function parseStructuredActions(job) {
|
|
|
922
995
|
actions.push({ type: "count_files", path: filePath, extensions, recursive });
|
|
923
996
|
continue;
|
|
924
997
|
}
|
|
998
|
+
if (type === "system_status") {
|
|
999
|
+
const validSections = Array.isArray(action.sections)
|
|
1000
|
+
? action.sections
|
|
1001
|
+
.map((item) => asString(item)?.toLowerCase())
|
|
1002
|
+
.filter((item) => item === "cpu" || item === "memory" || item === "disk" || item === "battery")
|
|
1003
|
+
: undefined;
|
|
1004
|
+
actions.push({
|
|
1005
|
+
type: "system_status",
|
|
1006
|
+
sections: validSections && validSections.length > 0 ? Array.from(new Set(validSections)) : undefined,
|
|
1007
|
+
include_top_processes: action.include_top_processes === true,
|
|
1008
|
+
});
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
925
1011
|
if (type === "run_shell" || type === "shell" || type === "terminal") {
|
|
926
1012
|
const command = asString(action.command) || asString(action.cmd);
|
|
927
1013
|
const cwd = asString(action.cwd);
|
|
@@ -1277,6 +1363,30 @@ export class NativeMacOSJobExecutor {
|
|
|
1277
1363
|
completionNotes.push(`Li a pagina ${page.title || page.url || "ativa"} no navegador.`);
|
|
1278
1364
|
continue;
|
|
1279
1365
|
}
|
|
1366
|
+
if (action.type === "browser_context") {
|
|
1367
|
+
await reporter.progress(progressPercent, "Lendo o contexto do navegador ativo");
|
|
1368
|
+
const browserContext = await this.collectBrowserContext(action.app, action.include_text === true, action.include_tabs === true);
|
|
1369
|
+
resultPayload.browser_context = browserContext;
|
|
1370
|
+
resultPayload.summary = browserContext.summary;
|
|
1371
|
+
completionNotes.push(browserContext.summary);
|
|
1372
|
+
continue;
|
|
1373
|
+
}
|
|
1374
|
+
if (action.type === "app_status") {
|
|
1375
|
+
await reporter.progress(progressPercent, "Lendo os apps ativos do Mac");
|
|
1376
|
+
const appStatus = await this.collectAppStatus(action.app, action.include_frontmost === true, action.include_running_apps === true, action.include_top_processes === true);
|
|
1377
|
+
resultPayload.app_status = appStatus;
|
|
1378
|
+
resultPayload.summary = appStatus.summary;
|
|
1379
|
+
completionNotes.push(appStatus.summary);
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
if (action.type === "filesystem_inspect") {
|
|
1383
|
+
await reporter.progress(progressPercent, `Inspecionando ${action.path}`);
|
|
1384
|
+
const filesystem = await this.inspectFilesystemPath(action.path, action.include_children === true, action.include_preview === true, action.limit);
|
|
1385
|
+
resultPayload.filesystem = filesystem;
|
|
1386
|
+
resultPayload.summary = filesystem.summary;
|
|
1387
|
+
completionNotes.push(filesystem.summary);
|
|
1388
|
+
continue;
|
|
1389
|
+
}
|
|
1280
1390
|
if (action.type === "read_file") {
|
|
1281
1391
|
await reporter.progress(progressPercent, `Lendo ${action.path}`);
|
|
1282
1392
|
const fileContent = await this.readLocalFile(action.path, action.max_chars);
|
|
@@ -1301,6 +1411,14 @@ export class NativeMacOSJobExecutor {
|
|
|
1301
1411
|
};
|
|
1302
1412
|
continue;
|
|
1303
1413
|
}
|
|
1414
|
+
if (action.type === "system_status") {
|
|
1415
|
+
await reporter.progress(progressPercent, "Lendo CPU, memoria, disco e bateria do Mac");
|
|
1416
|
+
const systemStatus = await this.collectSystemStatus(action.sections, action.include_top_processes === true);
|
|
1417
|
+
resultPayload.system_status = systemStatus;
|
|
1418
|
+
resultPayload.summary = systemStatus.summary;
|
|
1419
|
+
completionNotes.push(systemStatus.summary);
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1304
1422
|
if (action.type === "run_shell") {
|
|
1305
1423
|
await reporter.progress(progressPercent, `Rodando comando local: ${action.command}`);
|
|
1306
1424
|
const shellOutput = await this.runShellCommand(action.command, action.cwd);
|
|
@@ -1861,6 +1979,406 @@ end tell
|
|
|
1861
1979
|
}
|
|
1862
1980
|
return null;
|
|
1863
1981
|
}
|
|
1982
|
+
async resolveBrowserContextApp(preferredApp) {
|
|
1983
|
+
const candidates = [
|
|
1984
|
+
preferredApp || null,
|
|
1985
|
+
this.lastActiveApp,
|
|
1986
|
+
await this.getFrontmostAppName(),
|
|
1987
|
+
];
|
|
1988
|
+
for (const candidate of candidates) {
|
|
1989
|
+
if (candidate === "Safari" || candidate === "Google Chrome") {
|
|
1990
|
+
return candidate;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
return null;
|
|
1994
|
+
}
|
|
1995
|
+
async readCurrentBrowserMetadata(app) {
|
|
1996
|
+
const script = app === "Google Chrome"
|
|
1997
|
+
? `
|
|
1998
|
+
tell application "Google Chrome"
|
|
1999
|
+
activate
|
|
2000
|
+
if (count of windows) = 0 then error "Google Chrome nao possui janelas abertas."
|
|
2001
|
+
set pageTitle to title of active tab of front window
|
|
2002
|
+
set pageUrl to URL of active tab of front window
|
|
2003
|
+
end tell
|
|
2004
|
+
return pageTitle & linefeed & pageUrl
|
|
2005
|
+
`
|
|
2006
|
+
: `
|
|
2007
|
+
tell application "Safari"
|
|
2008
|
+
activate
|
|
2009
|
+
if (count of windows) = 0 then error "Safari nao possui janelas abertas."
|
|
2010
|
+
set pageTitle to name of current tab of front window
|
|
2011
|
+
set pageUrl to URL of current tab of front window
|
|
2012
|
+
end tell
|
|
2013
|
+
return pageTitle & linefeed & pageUrl
|
|
2014
|
+
`;
|
|
2015
|
+
const { stdout } = await this.runCommandCapture("osascript", ["-e", script]);
|
|
2016
|
+
const [title, url] = String(stdout || "").split("\n");
|
|
2017
|
+
return {
|
|
2018
|
+
title: String(title || "").trim(),
|
|
2019
|
+
url: String(url || "").trim(),
|
|
2020
|
+
};
|
|
2021
|
+
}
|
|
2022
|
+
async readCurrentBrowserPage(app) {
|
|
2023
|
+
if (app === "Safari") {
|
|
2024
|
+
const page = await this.readFrontmostPage("Safari");
|
|
2025
|
+
return {
|
|
2026
|
+
title: page.title,
|
|
2027
|
+
url: page.url,
|
|
2028
|
+
text: page.text,
|
|
2029
|
+
};
|
|
2030
|
+
}
|
|
2031
|
+
const script = `
|
|
2032
|
+
tell application "Google Chrome"
|
|
2033
|
+
activate
|
|
2034
|
+
if (count of windows) = 0 then error "Google Chrome nao possui janelas abertas."
|
|
2035
|
+
delay 0.5
|
|
2036
|
+
set jsCode to "(function(){const title=document.title||'';const url=location.href||'';const text=((document.body&&document.body.innerText)||'').trim().slice(0,12000);return JSON.stringify({title,url,text});})();"
|
|
2037
|
+
set pageJson to execute active tab of front window javascript jsCode
|
|
2038
|
+
end tell
|
|
2039
|
+
return pageJson
|
|
2040
|
+
`;
|
|
2041
|
+
try {
|
|
2042
|
+
const { stdout } = await this.runCommandCapture("osascript", ["-e", script]);
|
|
2043
|
+
const parsed = JSON.parse(String(stdout || "").trim() || "{}");
|
|
2044
|
+
return {
|
|
2045
|
+
title: asString(parsed.title) || "",
|
|
2046
|
+
url: asString(parsed.url) || "",
|
|
2047
|
+
text: asString(parsed.text) || "",
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
catch {
|
|
2051
|
+
const metadata = await this.readCurrentBrowserMetadata(app);
|
|
2052
|
+
return {
|
|
2053
|
+
...metadata,
|
|
2054
|
+
text: "",
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
async listBrowserTabs(app, limit = 8) {
|
|
2059
|
+
const script = app === "Google Chrome"
|
|
2060
|
+
? `
|
|
2061
|
+
tell application "Google Chrome"
|
|
2062
|
+
if (count of windows) = 0 then return ""
|
|
2063
|
+
set activeTabId to id of active tab of front window
|
|
2064
|
+
set outputLines to ""
|
|
2065
|
+
repeat with t in tabs of front window
|
|
2066
|
+
set marker to "0"
|
|
2067
|
+
if (id of t) is activeTabId then set marker to "1"
|
|
2068
|
+
set outputLines to outputLines & marker & tab & (title of t) & tab & (URL of t) & linefeed
|
|
2069
|
+
end repeat
|
|
2070
|
+
return outputLines
|
|
2071
|
+
end tell
|
|
2072
|
+
`
|
|
2073
|
+
: `
|
|
2074
|
+
tell application "Safari"
|
|
2075
|
+
if (count of windows) = 0 then return ""
|
|
2076
|
+
set activeIndex to index of current tab of front window
|
|
2077
|
+
set outputLines to ""
|
|
2078
|
+
set tabIndex to 0
|
|
2079
|
+
repeat with t in tabs of front window
|
|
2080
|
+
set tabIndex to tabIndex + 1
|
|
2081
|
+
set marker to "0"
|
|
2082
|
+
if tabIndex is activeIndex then set marker to "1"
|
|
2083
|
+
set outputLines to outputLines & marker & tab & (name of t) & tab & (URL of t) & linefeed
|
|
2084
|
+
end repeat
|
|
2085
|
+
return outputLines
|
|
2086
|
+
end tell
|
|
2087
|
+
`;
|
|
2088
|
+
try {
|
|
2089
|
+
const { stdout } = await this.runCommandCapture("osascript", ["-e", script]);
|
|
2090
|
+
return String(stdout || "")
|
|
2091
|
+
.split(/\r?\n/)
|
|
2092
|
+
.map((line) => line.trim())
|
|
2093
|
+
.filter(Boolean)
|
|
2094
|
+
.slice(0, limit)
|
|
2095
|
+
.map((line) => {
|
|
2096
|
+
const [marker, title, url] = line.split("\t");
|
|
2097
|
+
return {
|
|
2098
|
+
current: marker === "1",
|
|
2099
|
+
title: String(title || "").trim(),
|
|
2100
|
+
url: String(url || "").trim(),
|
|
2101
|
+
};
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
2104
|
+
catch {
|
|
2105
|
+
return [];
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
buildBrowserContextSummary(context, includeText, includeTabs) {
|
|
2109
|
+
const title = context.title || "sem titulo visivel";
|
|
2110
|
+
let summary = `${context.app} esta na aba "${title}"`;
|
|
2111
|
+
if (context.url) {
|
|
2112
|
+
summary += ` com a URL ${context.url}.`;
|
|
2113
|
+
}
|
|
2114
|
+
else {
|
|
2115
|
+
summary += ".";
|
|
2116
|
+
}
|
|
2117
|
+
if (includeText) {
|
|
2118
|
+
if (context.text) {
|
|
2119
|
+
summary += ` Conteudo visivel: ${clipTextPreview(context.text, 360)}.`;
|
|
2120
|
+
}
|
|
2121
|
+
else {
|
|
2122
|
+
summary += " Nao consegui extrair o texto visivel desta pagina desta vez.";
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
if (includeTabs && context.tabs && context.tabs.length > 0) {
|
|
2126
|
+
const tabsPreview = context.tabs
|
|
2127
|
+
.slice(0, 4)
|
|
2128
|
+
.map((tab) => `${tab.current ? "[atual] " : ""}${tab.title || tab.url || "sem titulo"}`)
|
|
2129
|
+
.join(" | ");
|
|
2130
|
+
if (tabsPreview) {
|
|
2131
|
+
summary += ` Abas visiveis: ${tabsPreview}.`;
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
return summary;
|
|
2135
|
+
}
|
|
2136
|
+
async collectBrowserContext(preferredApp, includeText = false, includeTabs = false) {
|
|
2137
|
+
const app = await this.resolveBrowserContextApp(preferredApp);
|
|
2138
|
+
if (!app) {
|
|
2139
|
+
throw new Error("Nao encontrei Safari ou Google Chrome em foco para inspecionar agora.");
|
|
2140
|
+
}
|
|
2141
|
+
const page = includeText
|
|
2142
|
+
? await this.readCurrentBrowserPage(app)
|
|
2143
|
+
: await this.readCurrentBrowserMetadata(app).then((metadata) => ({ ...metadata, text: "" }));
|
|
2144
|
+
const tabs = includeTabs ? await this.listBrowserTabs(app) : undefined;
|
|
2145
|
+
const context = {
|
|
2146
|
+
app,
|
|
2147
|
+
title: page.title,
|
|
2148
|
+
url: page.url,
|
|
2149
|
+
text: page.text ? clipTextPreview(page.text, 1200) : undefined,
|
|
2150
|
+
tabs,
|
|
2151
|
+
summary: "",
|
|
2152
|
+
};
|
|
2153
|
+
context.summary = this.buildBrowserContextSummary(context, includeText, includeTabs);
|
|
2154
|
+
return context;
|
|
2155
|
+
}
|
|
2156
|
+
async readRunningApps(limit = 12) {
|
|
2157
|
+
try {
|
|
2158
|
+
const { stdout } = await this.runCommandCapture("osascript", [
|
|
2159
|
+
"-e",
|
|
2160
|
+
`
|
|
2161
|
+
tell application "System Events"
|
|
2162
|
+
set appNames to name of every application process whose background only is false
|
|
2163
|
+
end tell
|
|
2164
|
+
set AppleScript's text item delimiters to linefeed
|
|
2165
|
+
return appNames as text
|
|
2166
|
+
`,
|
|
2167
|
+
]);
|
|
2168
|
+
return Array.from(new Set(String(stdout || "")
|
|
2169
|
+
.split(/\r?\n/)
|
|
2170
|
+
.map((line) => line.trim())
|
|
2171
|
+
.filter(Boolean))).slice(0, limit);
|
|
2172
|
+
}
|
|
2173
|
+
catch {
|
|
2174
|
+
return [];
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
appMatchesProcessName(app, processName) {
|
|
2178
|
+
const normalizedApp = normalizeText(app || "").replace(/\s+/g, " ").trim();
|
|
2179
|
+
const normalizedProcess = normalizeText(processName || "").replace(/\s+/g, " ").trim();
|
|
2180
|
+
if (!normalizedApp || !normalizedProcess) {
|
|
2181
|
+
return false;
|
|
2182
|
+
}
|
|
2183
|
+
return normalizedProcess === normalizedApp
|
|
2184
|
+
|| normalizedProcess.includes(normalizedApp)
|
|
2185
|
+
|| normalizedApp.includes(normalizedProcess);
|
|
2186
|
+
}
|
|
2187
|
+
async readAppTopProcesses(limit = 5, preferredApp) {
|
|
2188
|
+
try {
|
|
2189
|
+
const { stdout } = await this.runCommandCapture("/bin/ps", ["-Ao", "pid=,pcpu=,rss=,comm=", "-r"]);
|
|
2190
|
+
const lines = stdout.trim().split(/\r?\n/).filter(Boolean);
|
|
2191
|
+
const processes = lines
|
|
2192
|
+
.map((line) => {
|
|
2193
|
+
const match = line.match(/^\s*(\d+)\s+([0-9.]+)\s+(\d+)\s+(.+)$/);
|
|
2194
|
+
if (!match)
|
|
2195
|
+
return null;
|
|
2196
|
+
const processName = match[4].trim().split("/").pop() || match[4].trim();
|
|
2197
|
+
return {
|
|
2198
|
+
name: processName,
|
|
2199
|
+
cpu_percent: roundMetric(Number(match[2]), 1),
|
|
2200
|
+
memory_bytes: Math.max(0, Number(match[3])) * 1024,
|
|
2201
|
+
};
|
|
2202
|
+
})
|
|
2203
|
+
.filter((item) => item !== null);
|
|
2204
|
+
const filtered = preferredApp
|
|
2205
|
+
? processes.filter((item) => this.appMatchesProcessName(preferredApp, item.name))
|
|
2206
|
+
: processes;
|
|
2207
|
+
return filtered
|
|
2208
|
+
.filter((item) => item.cpu_percent > 0 || (item.memory_bytes || 0) > 0)
|
|
2209
|
+
.slice(0, limit);
|
|
2210
|
+
}
|
|
2211
|
+
catch {
|
|
2212
|
+
return [];
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
buildAppStatusSummary(status) {
|
|
2216
|
+
const parts = [];
|
|
2217
|
+
if (status.target_app?.name) {
|
|
2218
|
+
if (!status.target_app.running) {
|
|
2219
|
+
parts.push(`${status.target_app.name} nao esta aberto agora`);
|
|
2220
|
+
}
|
|
2221
|
+
else if (status.target_app.frontmost) {
|
|
2222
|
+
parts.push(`${status.target_app.name} esta aberto e em foco agora`);
|
|
2223
|
+
}
|
|
2224
|
+
else {
|
|
2225
|
+
parts.push(`${status.target_app.name} esta aberto`);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
else if (status.frontmost_app) {
|
|
2229
|
+
parts.push(`No foco agora esta ${status.frontmost_app}`);
|
|
2230
|
+
}
|
|
2231
|
+
if (status.running_apps && status.running_apps.length > 0) {
|
|
2232
|
+
const preview = status.running_apps.slice(0, 6).join(", ");
|
|
2233
|
+
parts.push(`apps abertos: ${preview}${status.running_apps.length > 6 ? ", ..." : ""}`);
|
|
2234
|
+
}
|
|
2235
|
+
let summary = parts.length > 0
|
|
2236
|
+
? `${parts[0]}.`
|
|
2237
|
+
: "Consegui ler o estado atual dos apps no seu Mac.";
|
|
2238
|
+
if (parts.length > 1) {
|
|
2239
|
+
summary += ` Também vejo ${parts.slice(1).join(". ")}.`;
|
|
2240
|
+
}
|
|
2241
|
+
if (status.top_processes && status.top_processes.length > 0) {
|
|
2242
|
+
const topPreview = status.top_processes
|
|
2243
|
+
.slice(0, 3)
|
|
2244
|
+
.map((item) => `${item.name} (${roundMetric(item.cpu_percent)}% CPU${item.memory_bytes ? `, ${formatBytesCompact(item.memory_bytes)}` : ""})`)
|
|
2245
|
+
.join(", ");
|
|
2246
|
+
if (topPreview) {
|
|
2247
|
+
summary += ` Maiores consumos agora: ${topPreview}.`;
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
return summary;
|
|
2251
|
+
}
|
|
2252
|
+
async collectAppStatus(preferredApp, includeFrontmost = true, includeRunningApps = true, includeTopProcesses = true) {
|
|
2253
|
+
const frontmostApp = includeFrontmost ? await this.getFrontmostAppName() : null;
|
|
2254
|
+
const runningApps = includeRunningApps ? await this.readRunningApps() : [];
|
|
2255
|
+
const normalizedRunningApps = Array.from(new Set(runningApps));
|
|
2256
|
+
const targetApp = preferredApp || undefined;
|
|
2257
|
+
const targetRunning = targetApp ? normalizedRunningApps.some((item) => normalizeText(item) === normalizeText(targetApp)) : undefined;
|
|
2258
|
+
const topProcesses = includeTopProcesses
|
|
2259
|
+
? await this.readAppTopProcesses(5, targetApp)
|
|
2260
|
+
: [];
|
|
2261
|
+
const status = {
|
|
2262
|
+
captured_at: new Date().toISOString(),
|
|
2263
|
+
hostname: os.hostname(),
|
|
2264
|
+
platform: process.platform,
|
|
2265
|
+
requested_app: targetApp,
|
|
2266
|
+
frontmost_app: frontmostApp || undefined,
|
|
2267
|
+
summary: "",
|
|
2268
|
+
};
|
|
2269
|
+
if (targetApp) {
|
|
2270
|
+
status.target_app = {
|
|
2271
|
+
name: targetApp,
|
|
2272
|
+
running: Boolean(targetRunning),
|
|
2273
|
+
frontmost: Boolean(frontmostApp && normalizeText(frontmostApp) === normalizeText(targetApp)),
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
if (includeRunningApps && normalizedRunningApps.length > 0) {
|
|
2277
|
+
status.running_apps = normalizedRunningApps;
|
|
2278
|
+
}
|
|
2279
|
+
if (includeTopProcesses && topProcesses && topProcesses.length > 0) {
|
|
2280
|
+
status.top_processes = topProcesses;
|
|
2281
|
+
}
|
|
2282
|
+
status.summary = this.buildAppStatusSummary(status);
|
|
2283
|
+
return status;
|
|
2284
|
+
}
|
|
2285
|
+
async resolveFilesystemInspectPath(targetPath) {
|
|
2286
|
+
const expanded = expandUserPath(targetPath);
|
|
2287
|
+
try {
|
|
2288
|
+
await stat(expanded);
|
|
2289
|
+
return expanded;
|
|
2290
|
+
}
|
|
2291
|
+
catch {
|
|
2292
|
+
// Continue below.
|
|
2293
|
+
}
|
|
2294
|
+
if (path.extname(expanded)) {
|
|
2295
|
+
return this.resolveReadableFilePath(targetPath);
|
|
2296
|
+
}
|
|
2297
|
+
return expanded;
|
|
2298
|
+
}
|
|
2299
|
+
async readDirectoryUsageBytes(targetPath) {
|
|
2300
|
+
try {
|
|
2301
|
+
const { stdout } = await this.runCommandCapture("/usr/bin/du", ["-sk", targetPath]);
|
|
2302
|
+
const match = String(stdout || "").match(/^\s*(\d+)/);
|
|
2303
|
+
if (!match) {
|
|
2304
|
+
return 0;
|
|
2305
|
+
}
|
|
2306
|
+
return Number(match[1]) * 1024;
|
|
2307
|
+
}
|
|
2308
|
+
catch {
|
|
2309
|
+
return 0;
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
async inspectFilesystemPath(targetPath, includeChildren = true, includePreview = false, limit = 8) {
|
|
2313
|
+
const resolved = await this.resolveFilesystemInspectPath(targetPath);
|
|
2314
|
+
const entryStat = await stat(resolved);
|
|
2315
|
+
const itemName = path.basename(resolved) || resolved;
|
|
2316
|
+
if (entryStat.isDirectory()) {
|
|
2317
|
+
const entries = await readdir(resolved, { withFileTypes: true });
|
|
2318
|
+
const slicedEntries = entries
|
|
2319
|
+
.sort((left, right) => {
|
|
2320
|
+
if (left.isDirectory() !== right.isDirectory()) {
|
|
2321
|
+
return left.isDirectory() ? -1 : 1;
|
|
2322
|
+
}
|
|
2323
|
+
return left.name.localeCompare(right.name);
|
|
2324
|
+
})
|
|
2325
|
+
.slice(0, Math.max(1, Math.min(limit, 20)));
|
|
2326
|
+
const children = includeChildren
|
|
2327
|
+
? await Promise.all(slicedEntries.map(async (entry) => {
|
|
2328
|
+
const childPath = path.join(resolved, entry.name);
|
|
2329
|
+
try {
|
|
2330
|
+
const childStat = await stat(childPath);
|
|
2331
|
+
return {
|
|
2332
|
+
name: entry.name,
|
|
2333
|
+
kind: entry.isDirectory() ? "directory" : "file",
|
|
2334
|
+
size_bytes: entry.isDirectory() ? undefined : childStat.size,
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
catch {
|
|
2338
|
+
return {
|
|
2339
|
+
name: entry.name,
|
|
2340
|
+
kind: entry.isDirectory() ? "directory" : "file",
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
}))
|
|
2344
|
+
: undefined;
|
|
2345
|
+
const totalSize = await this.readDirectoryUsageBytes(resolved);
|
|
2346
|
+
const snapshot = {
|
|
2347
|
+
captured_at: new Date().toISOString(),
|
|
2348
|
+
path: targetPath,
|
|
2349
|
+
resolved_path: resolved,
|
|
2350
|
+
kind: "directory",
|
|
2351
|
+
name: itemName,
|
|
2352
|
+
size_bytes: totalSize,
|
|
2353
|
+
modified_at: entryStat.mtime.toISOString(),
|
|
2354
|
+
item_count: entries.length,
|
|
2355
|
+
children,
|
|
2356
|
+
summary: "",
|
|
2357
|
+
};
|
|
2358
|
+
const childPreview = children && children.length > 0
|
|
2359
|
+
? children
|
|
2360
|
+
.slice(0, 5)
|
|
2361
|
+
.map((item) => `${item.name}${item.kind === "directory" ? "/" : ""}`)
|
|
2362
|
+
.join(", ")
|
|
2363
|
+
: "";
|
|
2364
|
+
snapshot.summary = `A pasta ${targetPath} tem ${entries.length} item${entries.length === 1 ? "" : "s"} e ocupa ${formatBytesCompact(totalSize)}.${childPreview ? ` Itens visiveis agora: ${childPreview}.` : ""}`;
|
|
2365
|
+
return snapshot;
|
|
2366
|
+
}
|
|
2367
|
+
const preview = includePreview ? await this.readLocalFile(resolved, 1200) : undefined;
|
|
2368
|
+
const snapshot = {
|
|
2369
|
+
captured_at: new Date().toISOString(),
|
|
2370
|
+
path: targetPath,
|
|
2371
|
+
resolved_path: resolved,
|
|
2372
|
+
kind: "file",
|
|
2373
|
+
name: itemName,
|
|
2374
|
+
size_bytes: entryStat.size,
|
|
2375
|
+
modified_at: entryStat.mtime.toISOString(),
|
|
2376
|
+
preview: preview || undefined,
|
|
2377
|
+
summary: "",
|
|
2378
|
+
};
|
|
2379
|
+
snapshot.summary = `O arquivo ${targetPath} pesa ${formatBytesCompact(entryStat.size)} e foi modificado em ${entryStat.mtime.toISOString()}.${preview ? ` Pre-visualizacao: ${clipTextPreview(preview, 320)}.` : ""}`;
|
|
2380
|
+
return snapshot;
|
|
2381
|
+
}
|
|
1864
2382
|
async captureBrowserPageState(app) {
|
|
1865
2383
|
if (app !== "Safari") {
|
|
1866
2384
|
return null;
|
|
@@ -4600,6 +5118,233 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
4600
5118
|
extensionsLabel,
|
|
4601
5119
|
};
|
|
4602
5120
|
}
|
|
5121
|
+
snapshotCpuTimes() {
|
|
5122
|
+
const cpus = os.cpus();
|
|
5123
|
+
let idle = 0;
|
|
5124
|
+
let total = 0;
|
|
5125
|
+
for (const cpu of cpus) {
|
|
5126
|
+
idle += cpu.times.idle;
|
|
5127
|
+
total += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.irq + cpu.times.idle;
|
|
5128
|
+
}
|
|
5129
|
+
return {
|
|
5130
|
+
idle,
|
|
5131
|
+
total,
|
|
5132
|
+
model: cpus[0]?.model || "Apple Silicon",
|
|
5133
|
+
logicalCores: cpus.length || 0,
|
|
5134
|
+
};
|
|
5135
|
+
}
|
|
5136
|
+
async sampleCpuStatus() {
|
|
5137
|
+
const start = this.snapshotCpuTimes();
|
|
5138
|
+
await delay(320);
|
|
5139
|
+
const end = this.snapshotCpuTimes();
|
|
5140
|
+
const totalDelta = Math.max(1, end.total - start.total);
|
|
5141
|
+
const idleDelta = Math.max(0, end.idle - start.idle);
|
|
5142
|
+
const idlePercent = roundMetric((idleDelta / totalDelta) * 100, 1);
|
|
5143
|
+
const usagePercent = roundMetric(Math.max(0, 100 - idlePercent), 1);
|
|
5144
|
+
const [load1m, load5m, load15m] = os.loadavg();
|
|
5145
|
+
return {
|
|
5146
|
+
usage_percent: usagePercent,
|
|
5147
|
+
idle_percent: idlePercent,
|
|
5148
|
+
logical_cores: end.logicalCores,
|
|
5149
|
+
model: end.model,
|
|
5150
|
+
load_average_1m: roundMetric(load1m, 2),
|
|
5151
|
+
load_average_5m: roundMetric(load5m, 2),
|
|
5152
|
+
load_average_15m: roundMetric(load15m, 2),
|
|
5153
|
+
};
|
|
5154
|
+
}
|
|
5155
|
+
async readMemoryStatus() {
|
|
5156
|
+
const totalBytes = os.totalmem();
|
|
5157
|
+
const freeBytes = os.freemem();
|
|
5158
|
+
const usedBytes = Math.max(0, totalBytes - freeBytes);
|
|
5159
|
+
let compressedBytes = 0;
|
|
5160
|
+
let swapUsedBytes = 0;
|
|
5161
|
+
try {
|
|
5162
|
+
const { stdout } = await this.runCommandCapture("/usr/bin/vm_stat", []);
|
|
5163
|
+
const pageSizeMatch = stdout.match(/page size of (\d+) bytes/i);
|
|
5164
|
+
const pageSize = pageSizeMatch ? Number(pageSizeMatch[1]) : 16384;
|
|
5165
|
+
const compressedMatch = stdout.match(/Pages occupied by compressor:\s+([0-9.]+)/i);
|
|
5166
|
+
if (compressedMatch) {
|
|
5167
|
+
compressedBytes = Math.round(Number(compressedMatch[1]) * pageSize);
|
|
5168
|
+
}
|
|
5169
|
+
}
|
|
5170
|
+
catch {
|
|
5171
|
+
compressedBytes = 0;
|
|
5172
|
+
}
|
|
5173
|
+
try {
|
|
5174
|
+
const { stdout } = await this.runCommandCapture("/usr/sbin/sysctl", ["vm.swapusage"]);
|
|
5175
|
+
const usedMatch = stdout.match(/used = ([0-9.]+)([BKMGTP]+)/i);
|
|
5176
|
+
if (usedMatch) {
|
|
5177
|
+
swapUsedBytes = parseScaledBytes(usedMatch[1], usedMatch[2]);
|
|
5178
|
+
}
|
|
5179
|
+
}
|
|
5180
|
+
catch {
|
|
5181
|
+
swapUsedBytes = 0;
|
|
5182
|
+
}
|
|
5183
|
+
const usedPercent = totalBytes > 0 ? roundMetric((usedBytes / totalBytes) * 100, 1) : 0;
|
|
5184
|
+
const pressure = (usedPercent >= 90 || swapUsedBytes >= 1.5 * 1024 ** 3)
|
|
5185
|
+
? "high"
|
|
5186
|
+
: (usedPercent >= 80 || swapUsedBytes >= 512 * 1024 ** 2 || compressedBytes >= 1024 ** 3)
|
|
5187
|
+
? "attention"
|
|
5188
|
+
: "normal";
|
|
5189
|
+
return {
|
|
5190
|
+
total_bytes: totalBytes,
|
|
5191
|
+
used_bytes: usedBytes,
|
|
5192
|
+
free_bytes: freeBytes,
|
|
5193
|
+
used_percent: usedPercent,
|
|
5194
|
+
pressure,
|
|
5195
|
+
swap_used_bytes: swapUsedBytes || undefined,
|
|
5196
|
+
compressed_bytes: compressedBytes || undefined,
|
|
5197
|
+
};
|
|
5198
|
+
}
|
|
5199
|
+
async readDiskStatus() {
|
|
5200
|
+
const { stdout } = await this.runCommandCapture("/bin/df", ["-k", "/"]);
|
|
5201
|
+
const lines = stdout.trim().split(/\r?\n/).filter(Boolean);
|
|
5202
|
+
if (lines.length < 2) {
|
|
5203
|
+
return undefined;
|
|
5204
|
+
}
|
|
5205
|
+
const parts = lines[1].trim().split(/\s+/);
|
|
5206
|
+
if (parts.length < 6) {
|
|
5207
|
+
return undefined;
|
|
5208
|
+
}
|
|
5209
|
+
const totalBytes = Number(parts[1]) * 1024;
|
|
5210
|
+
const usedBytes = Number(parts[2]) * 1024;
|
|
5211
|
+
const availableBytes = Number(parts[3]) * 1024;
|
|
5212
|
+
const usedPercent = roundMetric(Number((parts[4] || "").replace("%", "")), 1);
|
|
5213
|
+
return {
|
|
5214
|
+
mount_path: parts[5] || "/",
|
|
5215
|
+
total_bytes: totalBytes,
|
|
5216
|
+
used_bytes: usedBytes,
|
|
5217
|
+
available_bytes: availableBytes,
|
|
5218
|
+
used_percent: usedPercent,
|
|
5219
|
+
available_gb: roundMetric(availableBytes / (1024 ** 3), 1),
|
|
5220
|
+
};
|
|
5221
|
+
}
|
|
5222
|
+
async readBatteryStatus() {
|
|
5223
|
+
try {
|
|
5224
|
+
const { stdout } = await this.runCommandCapture("/usr/bin/pmset", ["-g", "batt"]);
|
|
5225
|
+
const percentageMatch = stdout.match(/(\d+)%/);
|
|
5226
|
+
if (!percentageMatch) {
|
|
5227
|
+
return undefined;
|
|
5228
|
+
}
|
|
5229
|
+
const powerSourceMatch = stdout.match(/Now drawing from '([^']+)'/i);
|
|
5230
|
+
const powerSource = powerSourceMatch?.[1]?.trim() || "Unknown";
|
|
5231
|
+
const normalized = stdout.toLowerCase();
|
|
5232
|
+
const charging = normalized.includes("charging") || normalized.includes("charged") || powerSource.toLowerCase().includes("ac");
|
|
5233
|
+
return {
|
|
5234
|
+
percentage: Math.max(0, Math.min(100, Number(percentageMatch[1]))),
|
|
5235
|
+
charging,
|
|
5236
|
+
power_source: powerSource,
|
|
5237
|
+
};
|
|
5238
|
+
}
|
|
5239
|
+
catch {
|
|
5240
|
+
return undefined;
|
|
5241
|
+
}
|
|
5242
|
+
}
|
|
5243
|
+
async readTopProcesses(limit = 4) {
|
|
5244
|
+
try {
|
|
5245
|
+
const { stdout } = await this.runCommandCapture("/bin/ps", ["-Ao", "pcpu,comm", "-r"]);
|
|
5246
|
+
const lines = stdout.trim().split(/\r?\n/).slice(1);
|
|
5247
|
+
return lines
|
|
5248
|
+
.map((line) => line.trim())
|
|
5249
|
+
.filter(Boolean)
|
|
5250
|
+
.slice(0, limit)
|
|
5251
|
+
.map((line) => {
|
|
5252
|
+
const match = line.match(/^([0-9.]+)\s+(.+)$/);
|
|
5253
|
+
if (!match)
|
|
5254
|
+
return null;
|
|
5255
|
+
const cpuPercent = roundMetric(Number(match[1]), 1);
|
|
5256
|
+
const command = match[2].trim().split("/").pop() || match[2].trim();
|
|
5257
|
+
return {
|
|
5258
|
+
name: command,
|
|
5259
|
+
cpu_percent: cpuPercent,
|
|
5260
|
+
};
|
|
5261
|
+
})
|
|
5262
|
+
.filter((item) => item !== null && item.cpu_percent > 0);
|
|
5263
|
+
}
|
|
5264
|
+
catch {
|
|
5265
|
+
return [];
|
|
5266
|
+
}
|
|
5267
|
+
}
|
|
5268
|
+
buildSystemStatusSummary(status) {
|
|
5269
|
+
const parts = [];
|
|
5270
|
+
const warnings = [];
|
|
5271
|
+
if (status.cpu) {
|
|
5272
|
+
parts.push(`CPU em ${roundMetric(status.cpu.usage_percent)}%`);
|
|
5273
|
+
if (status.cpu.usage_percent >= 85) {
|
|
5274
|
+
warnings.push(`CPU em ${roundMetric(status.cpu.usage_percent)}%`);
|
|
5275
|
+
}
|
|
5276
|
+
}
|
|
5277
|
+
if (status.memory) {
|
|
5278
|
+
parts.push(`memoria em ${roundMetric(status.memory.used_percent)}%`);
|
|
5279
|
+
if (status.memory.pressure === "high") {
|
|
5280
|
+
warnings.push(`memoria pressionada (${roundMetric(status.memory.used_percent)}% e swap ativo)`);
|
|
5281
|
+
}
|
|
5282
|
+
else if (status.memory.pressure === "attention") {
|
|
5283
|
+
warnings.push(`memoria em ${roundMetric(status.memory.used_percent)}%`);
|
|
5284
|
+
}
|
|
5285
|
+
}
|
|
5286
|
+
if (status.disk) {
|
|
5287
|
+
parts.push(`${formatBytesCompact(status.disk.available_bytes)} livres no disco`);
|
|
5288
|
+
if (status.disk.used_percent >= 90 || status.disk.available_bytes <= 15 * 1024 ** 3) {
|
|
5289
|
+
warnings.push(`pouco espaco livre (${formatBytesCompact(status.disk.available_bytes)})`);
|
|
5290
|
+
}
|
|
5291
|
+
}
|
|
5292
|
+
if (status.battery) {
|
|
5293
|
+
parts.push(`bateria em ${status.battery.percentage}%${status.battery.charging ? " carregando" : ""}`);
|
|
5294
|
+
if (!status.battery.charging && status.battery.percentage <= 20) {
|
|
5295
|
+
warnings.push(`bateria em ${status.battery.percentage}%`);
|
|
5296
|
+
}
|
|
5297
|
+
}
|
|
5298
|
+
let summary = warnings.length > 0
|
|
5299
|
+
? `Seu Mac esta operando, mas merece atencao em ${warnings.join(" e ")}.`
|
|
5300
|
+
: "No geral, seu Mac esta de boa.";
|
|
5301
|
+
if (parts.length > 0) {
|
|
5302
|
+
summary += ` Agora vejo ${parts.join(", ")}.`;
|
|
5303
|
+
}
|
|
5304
|
+
if (status.top_processes && status.top_processes.length > 0) {
|
|
5305
|
+
const topProcesses = status.top_processes
|
|
5306
|
+
.slice(0, 3)
|
|
5307
|
+
.map((item) => `${item.name} (${roundMetric(item.cpu_percent)}%)`)
|
|
5308
|
+
.join(", ");
|
|
5309
|
+
if (topProcesses) {
|
|
5310
|
+
summary += ` Maiores consumos agora: ${topProcesses}.`;
|
|
5311
|
+
}
|
|
5312
|
+
}
|
|
5313
|
+
return summary;
|
|
5314
|
+
}
|
|
5315
|
+
async collectSystemStatus(sections, includeTopProcesses = true) {
|
|
5316
|
+
const requestedSections = sections && sections.length > 0
|
|
5317
|
+
? sections
|
|
5318
|
+
: ["cpu", "memory", "disk", "battery"];
|
|
5319
|
+
const uniqueSections = Array.from(new Set(requestedSections));
|
|
5320
|
+
const status = {
|
|
5321
|
+
captured_at: new Date().toISOString(),
|
|
5322
|
+
hostname: os.hostname(),
|
|
5323
|
+
platform: process.platform,
|
|
5324
|
+
requested_sections: uniqueSections,
|
|
5325
|
+
summary: "",
|
|
5326
|
+
};
|
|
5327
|
+
if (uniqueSections.includes("cpu")) {
|
|
5328
|
+
status.cpu = await this.sampleCpuStatus();
|
|
5329
|
+
}
|
|
5330
|
+
if (uniqueSections.includes("memory")) {
|
|
5331
|
+
status.memory = await this.readMemoryStatus();
|
|
5332
|
+
}
|
|
5333
|
+
if (uniqueSections.includes("disk")) {
|
|
5334
|
+
status.disk = await this.readDiskStatus();
|
|
5335
|
+
}
|
|
5336
|
+
if (uniqueSections.includes("battery")) {
|
|
5337
|
+
status.battery = await this.readBatteryStatus();
|
|
5338
|
+
}
|
|
5339
|
+
if (includeTopProcesses && (uniqueSections.includes("cpu") || uniqueSections.includes("memory"))) {
|
|
5340
|
+
const topProcesses = await this.readTopProcesses();
|
|
5341
|
+
if (topProcesses && topProcesses.length > 0) {
|
|
5342
|
+
status.top_processes = topProcesses;
|
|
5343
|
+
}
|
|
5344
|
+
}
|
|
5345
|
+
status.summary = this.buildSystemStatusSummary(status);
|
|
5346
|
+
return status;
|
|
5347
|
+
}
|
|
4603
5348
|
async runShellCommand(command, cwd) {
|
|
4604
5349
|
if (!isSafeShellCommand(command)) {
|
|
4605
5350
|
throw new Error("Nenhum comando shell foi informado para execucao local.");
|
|
@@ -4653,6 +5398,15 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
4653
5398
|
if (action.type === "read_frontmost_page") {
|
|
4654
5399
|
return `Pagina ativa lida em ${action.app || "Safari"}`;
|
|
4655
5400
|
}
|
|
5401
|
+
if (action.type === "browser_context") {
|
|
5402
|
+
return `Contexto do navegador coletado${action.app ? ` em ${action.app}` : ""}`;
|
|
5403
|
+
}
|
|
5404
|
+
if (action.type === "app_status") {
|
|
5405
|
+
return `Status de apps coletado${action.app ? ` para ${action.app}` : ""}`;
|
|
5406
|
+
}
|
|
5407
|
+
if (action.type === "filesystem_inspect") {
|
|
5408
|
+
return `Inspecao local concluida em ${action.path}`;
|
|
5409
|
+
}
|
|
4656
5410
|
if (action.type === "read_file") {
|
|
4657
5411
|
return `${action.path} foi lido no macOS`;
|
|
4658
5412
|
}
|
|
@@ -4662,6 +5416,9 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
4662
5416
|
if (action.type === "count_files") {
|
|
4663
5417
|
return `Arquivos contados em ${action.path}`;
|
|
4664
5418
|
}
|
|
5419
|
+
if (action.type === "system_status") {
|
|
5420
|
+
return "Status do macOS coletado";
|
|
5421
|
+
}
|
|
4665
5422
|
if (action.type === "run_shell") {
|
|
4666
5423
|
return `Comando ${action.command} executado no macOS`;
|
|
4667
5424
|
}
|
package/dist/runtime.js
CHANGED
|
@@ -5,6 +5,7 @@ import { NativeMacOSJobExecutor } from "./executors/native_macos.js";
|
|
|
5
5
|
import { JobCancelledError } from "./executors/shared.js";
|
|
6
6
|
import { isManagedBridgeExtensionSlug, loadManagedBridgeExtensionState, } from "./extensions.js";
|
|
7
7
|
import { LocalAutomationRuntime } from "./local_automations.js";
|
|
8
|
+
import { buildLocalToolCatalog } from "./tool_catalog.js";
|
|
8
9
|
function delay(ms) {
|
|
9
10
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
11
|
}
|
|
@@ -89,6 +90,7 @@ export class BridgeRuntime {
|
|
|
89
90
|
const metadata = {
|
|
90
91
|
...(this.config.metadata || {}),
|
|
91
92
|
installed_extensions: this.config.installedExtensions,
|
|
93
|
+
executor_type: this.config.executor.type,
|
|
92
94
|
};
|
|
93
95
|
const managedExtensions = {};
|
|
94
96
|
const connectedMessageApps = new Set();
|
|
@@ -125,9 +127,21 @@ export class BridgeRuntime {
|
|
|
125
127
|
if (Object.keys(managedExtensions).length > 0) {
|
|
126
128
|
metadata.managed_extensions = managedExtensions;
|
|
127
129
|
}
|
|
130
|
+
else {
|
|
131
|
+
delete metadata.managed_extensions;
|
|
132
|
+
}
|
|
128
133
|
if (connectedMessageApps.size > 0) {
|
|
129
134
|
metadata.connected_message_apps = Array.from(connectedMessageApps);
|
|
130
135
|
}
|
|
136
|
+
else {
|
|
137
|
+
delete metadata.connected_message_apps;
|
|
138
|
+
}
|
|
139
|
+
const toolCatalog = buildLocalToolCatalog({
|
|
140
|
+
executorType: this.config.executor.type,
|
|
141
|
+
connectedMessageApps: metadata.connected_message_apps,
|
|
142
|
+
});
|
|
143
|
+
metadata.local_tools_version = toolCatalog.version;
|
|
144
|
+
metadata.local_tools = toolCatalog.tools;
|
|
131
145
|
return metadata;
|
|
132
146
|
}
|
|
133
147
|
async sendHello(socket) {
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { BRIDGE_LOCAL_TOOLS_VERSION, } from "./types.js";
|
|
2
|
+
const NATIVE_MACOS_BASE_TOOLS = [
|
|
3
|
+
{
|
|
4
|
+
id: "desktop.apps",
|
|
5
|
+
title: "Abrir e focar apps",
|
|
6
|
+
description: "Abre aplicativos locais e traz uma janela para frente no macOS.",
|
|
7
|
+
category: "desktop",
|
|
8
|
+
mode: "control",
|
|
9
|
+
action_types: ["open_app", "focus_app"],
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
id: "browser.navigation",
|
|
13
|
+
title: "Abrir links e destinos web",
|
|
14
|
+
description: "Abre URLs e pesquisas locais no navegador.",
|
|
15
|
+
category: "browser",
|
|
16
|
+
mode: "control",
|
|
17
|
+
action_types: ["open_url"],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: "content.notes",
|
|
21
|
+
title: "Criar notas e texto",
|
|
22
|
+
description: "Cria notas, digita texto e executa atalhos locais de escrita.",
|
|
23
|
+
category: "content",
|
|
24
|
+
mode: "write",
|
|
25
|
+
action_types: ["create_note", "type_text", "press_shortcut"],
|
|
26
|
+
requires_confirmation: true,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "desktop.capture",
|
|
30
|
+
title: "Captura de tela",
|
|
31
|
+
description: "Tira screenshot local do macOS.",
|
|
32
|
+
category: "desktop",
|
|
33
|
+
mode: "observe",
|
|
34
|
+
action_types: ["take_screenshot"],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "browser.context",
|
|
38
|
+
title: "Leitura do navegador",
|
|
39
|
+
description: "Le a pagina frontal e coleta contexto da aba atual do navegador.",
|
|
40
|
+
category: "browser",
|
|
41
|
+
mode: "observe",
|
|
42
|
+
action_types: ["read_frontmost_page", "browser_context"],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "system.apps",
|
|
46
|
+
title: "Status de apps",
|
|
47
|
+
description: "Checa app em foco, apps abertos e processos pesados.",
|
|
48
|
+
category: "system",
|
|
49
|
+
mode: "observe",
|
|
50
|
+
action_types: ["app_status"],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "filesystem.overview",
|
|
54
|
+
title: "Inspecao de arquivos e pastas",
|
|
55
|
+
description: "Inspeciona caminhos locais, lista itens e conta arquivos.",
|
|
56
|
+
category: "filesystem",
|
|
57
|
+
mode: "observe",
|
|
58
|
+
action_types: ["filesystem_inspect", "list_files", "count_files"],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "filesystem.read",
|
|
62
|
+
title: "Leitura de arquivo",
|
|
63
|
+
description: "Le arquivos de texto quando o caminho esta claro.",
|
|
64
|
+
category: "filesystem",
|
|
65
|
+
mode: "observe",
|
|
66
|
+
action_types: ["read_file"],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "system.health",
|
|
70
|
+
title: "Status do Mac",
|
|
71
|
+
description: "Coleta CPU, memoria, disco, bateria e processos de topo.",
|
|
72
|
+
category: "system",
|
|
73
|
+
mode: "observe",
|
|
74
|
+
action_types: ["system_status"],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "shell.inspect",
|
|
78
|
+
title: "Shell local",
|
|
79
|
+
description: "Roda comandos locais de consulta e diagnostico.",
|
|
80
|
+
category: "shell",
|
|
81
|
+
mode: "control",
|
|
82
|
+
action_types: ["run_shell"],
|
|
83
|
+
requires_confirmation: true,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "desktop.audio",
|
|
87
|
+
title: "Controle de volume",
|
|
88
|
+
description: "Ajusta o volume do macOS.",
|
|
89
|
+
category: "desktop",
|
|
90
|
+
mode: "control",
|
|
91
|
+
action_types: ["set_volume"],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "desktop.scroll",
|
|
95
|
+
title: "Scroll da tela",
|
|
96
|
+
description: "Rola a view ativa para cima ou para baixo.",
|
|
97
|
+
category: "desktop",
|
|
98
|
+
mode: "control",
|
|
99
|
+
action_types: ["scroll_view"],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "visual.click",
|
|
103
|
+
title: "Clique visual guiado",
|
|
104
|
+
description: "Procura um alvo simples na tela e clica nele.",
|
|
105
|
+
category: "visual",
|
|
106
|
+
mode: "control",
|
|
107
|
+
action_types: ["click_visual_target"],
|
|
108
|
+
requires_confirmation: true,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: "visual.drag",
|
|
112
|
+
title: "Arraste visual guiado",
|
|
113
|
+
description: "Arrasta um alvo visivel para outro alvo visivel.",
|
|
114
|
+
category: "visual",
|
|
115
|
+
mode: "control",
|
|
116
|
+
action_types: ["drag_visual_target"],
|
|
117
|
+
requires_confirmation: true,
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
const MOCK_TOOLS = [
|
|
121
|
+
{
|
|
122
|
+
id: "mock.runtime",
|
|
123
|
+
title: "Execucao mock",
|
|
124
|
+
description: "Simula a execucao local para testes sem efeitos reais no dispositivo.",
|
|
125
|
+
category: "desktop",
|
|
126
|
+
mode: "control",
|
|
127
|
+
action_types: [],
|
|
128
|
+
notes: "Os resultados sao sinteticos e servem apenas para desenvolvimento.",
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
const CLAWD_CURSOR_TOOLS = [
|
|
132
|
+
{
|
|
133
|
+
id: "clawd.cursor.task",
|
|
134
|
+
title: "Tarefa aberta no executor",
|
|
135
|
+
description: "Delegacao geral para o runtime Clawd Cursor executar uma tarefa no computador.",
|
|
136
|
+
category: "desktop",
|
|
137
|
+
mode: "control",
|
|
138
|
+
action_types: [],
|
|
139
|
+
requires_confirmation: true,
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
function hasConnectedMessageApp(apps, target) {
|
|
143
|
+
return apps.some((item) => item === target);
|
|
144
|
+
}
|
|
145
|
+
function buildNativeMessagingTools(connectedMessageApps) {
|
|
146
|
+
const tools = [];
|
|
147
|
+
if (hasConnectedMessageApp(connectedMessageApps, "whatsapp")) {
|
|
148
|
+
tools.push({
|
|
149
|
+
id: "messaging.whatsapp.read",
|
|
150
|
+
title: "Leitura de WhatsApp",
|
|
151
|
+
description: "Abre uma conversa no WhatsApp Web e le as mensagens visiveis.",
|
|
152
|
+
category: "messaging",
|
|
153
|
+
mode: "observe",
|
|
154
|
+
action_types: ["whatsapp_read_chat"],
|
|
155
|
+
}, {
|
|
156
|
+
id: "messaging.whatsapp.send",
|
|
157
|
+
title: "Envio de WhatsApp",
|
|
158
|
+
description: "Abre a conversa certa e envia mensagem no WhatsApp Web.",
|
|
159
|
+
category: "messaging",
|
|
160
|
+
mode: "write",
|
|
161
|
+
action_types: ["whatsapp_send_message"],
|
|
162
|
+
requires_confirmation: true,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return tools;
|
|
166
|
+
}
|
|
167
|
+
export function buildLocalToolCatalog(context) {
|
|
168
|
+
const connectedMessageApps = Array.from(new Set((context.connectedMessageApps || [])
|
|
169
|
+
.map((item) => String(item || "").trim().toLowerCase())
|
|
170
|
+
.filter(Boolean)));
|
|
171
|
+
if (context.executorType === "native-macos") {
|
|
172
|
+
return {
|
|
173
|
+
version: BRIDGE_LOCAL_TOOLS_VERSION,
|
|
174
|
+
executor_type: context.executorType,
|
|
175
|
+
tools: [
|
|
176
|
+
...NATIVE_MACOS_BASE_TOOLS,
|
|
177
|
+
...buildNativeMessagingTools(connectedMessageApps),
|
|
178
|
+
],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (context.executorType === "clawd-cursor") {
|
|
182
|
+
return {
|
|
183
|
+
version: BRIDGE_LOCAL_TOOLS_VERSION,
|
|
184
|
+
executor_type: context.executorType,
|
|
185
|
+
tools: CLAWD_CURSOR_TOOLS,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
version: BRIDGE_LOCAL_TOOLS_VERSION,
|
|
190
|
+
executor_type: context.executorType,
|
|
191
|
+
tools: MOCK_TOOLS,
|
|
192
|
+
};
|
|
193
|
+
}
|
package/dist/types.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const BRIDGE_CONFIG_VERSION = 1;
|
|
2
|
-
export const BRIDGE_VERSION = "0.
|
|
2
|
+
export const BRIDGE_VERSION = "0.9.0";
|
|
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,3 +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 = 1;
|