@leg3ndy/otto-bridge 0.8.3 → 0.8.5

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.
@@ -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/types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const BRIDGE_CONFIG_VERSION = 1;
2
- export const BRIDGE_VERSION = "0.8.3";
2
+ export const BRIDGE_VERSION = "0.8.5";
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leg3ndy/otto-bridge",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Local companion for Otto Bridge device pairing and WebSocket runtime.",