farai 0.3.4 → 0.3.6

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/dist/cli/index.js CHANGED
@@ -8784,8 +8784,8 @@ var init_process_output = __esm(() => {
8784
8784
 
8785
8785
  // src/version.ts
8786
8786
  function resolveFaraiVersion() {
8787
- if ("0.3.4")
8788
- return "0.3.4";
8787
+ if ("0.3.6")
8788
+ return "0.3.6";
8789
8789
  try {
8790
8790
  const parsed = JSON.parse(readBoundedFileTextSync(new URL("../package.json", import.meta.url), 1024 * 1024, "package metadata"));
8791
8791
  if (typeof parsed.version === "string" && parsed.version)
@@ -28681,6 +28681,2019 @@ var init_email = __esm(() => {
28681
28681
  emailTools = [emailListTool, emailCreateTool, emailInboxTool, emailReadTool, emailWaitTool];
28682
28682
  });
28683
28683
 
28684
+ // src/agent-tools/android/shared.ts
28685
+ function shellQuote7(value) {
28686
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
28687
+ }
28688
+ function adbUnavailable(result) {
28689
+ return result.exitCode === 127 || /adb: not found|command not found/i.test(result.stderr);
28690
+ }
28691
+ function compactError2(value) {
28692
+ const compact = value.replace(/\s+/g, " ").trim();
28693
+ return compact.slice(0, 400) || "adb command produced no error output";
28694
+ }
28695
+ async function runAdb(context, argline, timeoutMs, maxBytes = 2000000) {
28696
+ return backend(context).exec(argline, timeoutMs, context.signal, maxBytes);
28697
+ }
28698
+ function adbEnvPrefix() {
28699
+ const parts = ADB_SERVER_ENV.filter((name) => (process.env[name] ?? "").trim()).map((name) => `${name}=${shellQuote7(process.env[name].trim())}`);
28700
+ return parts.length ? `${parts.join(" ")} ` : "";
28701
+ }
28702
+ function adbBase() {
28703
+ return `${adbEnvPrefix()}adb`;
28704
+ }
28705
+ function adbPrefix(serial) {
28706
+ const trimmed = serial?.trim();
28707
+ return trimmed ? `${adbBase()} -s ${shellQuote7(trimmed)}` : adbBase();
28708
+ }
28709
+ function parseDevices(stdout) {
28710
+ const devices = [];
28711
+ for (const line of stdout.split(`
28712
+ `)) {
28713
+ const trimmed = line.trim();
28714
+ if (!trimmed || /^List of devices/i.test(trimmed))
28715
+ continue;
28716
+ const [serial, state, ...rest] = trimmed.split(/\s+/);
28717
+ if (!serial || !state)
28718
+ continue;
28719
+ const meta = rest.join(" ");
28720
+ const model = /\bmodel:(\S+)/.exec(meta)?.[1];
28721
+ const product = /\bproduct:(\S+)/.exec(meta)?.[1];
28722
+ devices.push({
28723
+ serial,
28724
+ state,
28725
+ ...model ? {
28726
+ model
28727
+ } : {},
28728
+ ...product ? {
28729
+ product
28730
+ } : {}
28731
+ });
28732
+ }
28733
+ return devices;
28734
+ }
28735
+ async function resolveDevice(context, serial, timeoutMs = 15000) {
28736
+ const provided = serial?.trim();
28737
+ if (provided)
28738
+ return provided;
28739
+ const result = await runAdb(context, `${adbBase()} devices -l`, timeoutMs);
28740
+ if (adbUnavailable(result))
28741
+ throw new Error("adb is not available in the container");
28742
+ const online = parseDevices(result.stdout).filter((device) => device.state === "device");
28743
+ if (online.length === 0)
28744
+ throw new Error("no android device is connected; use android_connect to attach one over tcp/ip");
28745
+ if (online.length > 1)
28746
+ throw new Error(`multiple devices connected (${online.map((device) => device.serial).join(", ")}); pass the serial argument`);
28747
+ return online[0].serial;
28748
+ }
28749
+ var ADB_SERVER_ENV;
28750
+ var init_shared4 = __esm(() => {
28751
+ init_backend();
28752
+ ADB_SERVER_ENV = ["ADB_SERVER_SOCKET", "ANDROID_ADB_SERVER_ADDRESS", "ANDROID_ADB_SERVER_PORT"];
28753
+ });
28754
+
28755
+ // src/agent-tools/android/device.ts
28756
+ var SERIAL_PROP, androidConnectTool, androidDevicesTool, androidShellTool, androidPackagesTool, androidDeviceInfoTool, androidLogcatTool;
28757
+ var init_device = __esm(() => {
28758
+ init_renderers();
28759
+ init_shared4();
28760
+ SERIAL_PROP = {
28761
+ type: "string",
28762
+ description: "device serial from android_devices; omit when exactly one device is connected"
28763
+ };
28764
+ androidConnectTool = {
28765
+ name: "android_connect",
28766
+ description: "Attach an android device over adb tcp/ip. Use this first when the device is reachable by wireless debugging, an emulator, or a remote host, since usb passthrough is unavailable inside the container. Provide host:port (default port 5555).",
28767
+ inputSchema: {
28768
+ type: "object",
28769
+ required: ["address"],
28770
+ properties: {
28771
+ address: {
28772
+ type: "string",
28773
+ description: "device address as host or host:port; port defaults to 5555 when omitted"
28774
+ }
28775
+ },
28776
+ additionalProperties: false
28777
+ },
28778
+ mutates: true,
28779
+ timeoutMs: 30000,
28780
+ parallel: false,
28781
+ renderHuman: defaultHumanRenderer,
28782
+ renderModel: defaultModelRenderer,
28783
+ run: async (args, context) => {
28784
+ assertObject(args, "args");
28785
+ const raw = asString(args.address, "address").trim();
28786
+ const address = /:\d+$/.test(raw) ? raw : `${raw}:5555`;
28787
+ const result = await runAdb(context, `${adbBase()} connect ${shellQuote7(address)}`, 30000);
28788
+ if (adbUnavailable(result))
28789
+ throw new Error("adb is not available in the container");
28790
+ const text2 = `${result.stdout}${result.stderr}`.trim();
28791
+ const ok = /connected to/i.test(text2) && !/cannot|failed|unable|refused/i.test(text2);
28792
+ return {
28793
+ ok,
28794
+ summary: ok ? `connected to ${address}` : `could not connect to ${address}`,
28795
+ output: text2 || `no output; exit ${result.exitCode}`,
28796
+ metadata: {
28797
+ address,
28798
+ connected: ok
28799
+ }
28800
+ };
28801
+ }
28802
+ };
28803
+ androidDevicesTool = {
28804
+ name: "android_devices",
28805
+ description: "List android devices adb can currently see, with serial, connection state, and model. Use this to pick a serial before other android tools, or to confirm android_connect worked.",
28806
+ inputSchema: {
28807
+ type: "object",
28808
+ properties: {},
28809
+ additionalProperties: false
28810
+ },
28811
+ mutates: false,
28812
+ timeoutMs: 15000,
28813
+ parallel: true,
28814
+ renderHuman: defaultHumanRenderer,
28815
+ renderModel: defaultModelRenderer,
28816
+ run: async (args, context) => {
28817
+ assertObject(args, "args");
28818
+ const result = await runAdb(context, `${adbBase()} devices -l`, 15000);
28819
+ if (adbUnavailable(result))
28820
+ throw new Error("adb is not available in the container");
28821
+ const devices = parseDevices(result.stdout);
28822
+ const online = devices.filter((device) => device.state === "device");
28823
+ const output = devices.length ? devices.map((device) => `${device.serial} ${device.state}${device.model ? ` model:${device.model}` : ""}`).join(`
28824
+ `) : "no devices";
28825
+ return {
28826
+ ok: true,
28827
+ summary: `${devices.length} device(s), ${online.length} online`,
28828
+ output,
28829
+ metadata: {
28830
+ devices
28831
+ }
28832
+ };
28833
+ }
28834
+ };
28835
+ androidShellTool = {
28836
+ name: "android_shell",
28837
+ description: "Run one shell command on the android device via adb shell. Use purpose-built android tools when they model the task; use this for arbitrary on-device commands, dumpsys, pm, or content queries.",
28838
+ inputSchema: {
28839
+ type: "object",
28840
+ required: ["command"],
28841
+ properties: {
28842
+ command: {
28843
+ type: "string",
28844
+ description: "complete shell command to run inside adb shell on the device"
28845
+ },
28846
+ serial: SERIAL_PROP
28847
+ },
28848
+ additionalProperties: false
28849
+ },
28850
+ mutates: true,
28851
+ timeoutMs: 60000,
28852
+ parallel: false,
28853
+ renderHuman: defaultHumanRenderer,
28854
+ renderModel: defaultModelRenderer,
28855
+ run: async (args, context) => {
28856
+ assertObject(args, "args");
28857
+ const command = asString(args.command, "command");
28858
+ const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
28859
+ const result = await runAdb(context, `${adbPrefix(serial)} shell ${shellQuote7(command)}`, 60000);
28860
+ if (adbUnavailable(result))
28861
+ throw new Error("adb is not available in the container");
28862
+ const output = `${result.stdout}${result.stderr ? `
28863
+ ${result.stderr}` : ""}`.trim();
28864
+ return {
28865
+ ok: result.exitCode === 0,
28866
+ summary: result.exitCode === 0 ? `ran on ${serial}` : `command exited ${result.exitCode} on ${serial}`,
28867
+ output: output || "(no output)",
28868
+ metadata: {
28869
+ serial,
28870
+ exitCode: result.exitCode
28871
+ }
28872
+ };
28873
+ }
28874
+ };
28875
+ androidPackagesTool = {
28876
+ name: "android_packages",
28877
+ description: "List installed packages on the device. Use thirdPartyOnly to focus on user-installed apps, and filter to narrow by substring.",
28878
+ inputSchema: {
28879
+ type: "object",
28880
+ properties: {
28881
+ filter: {
28882
+ type: "string",
28883
+ description: "case-insensitive substring to match against package names"
28884
+ },
28885
+ thirdPartyOnly: {
28886
+ type: "boolean",
28887
+ description: "list only user-installed apps (pm list packages -3) when true"
28888
+ },
28889
+ serial: SERIAL_PROP
28890
+ },
28891
+ additionalProperties: false
28892
+ },
28893
+ mutates: false,
28894
+ timeoutMs: 30000,
28895
+ parallel: true,
28896
+ renderHuman: defaultHumanRenderer,
28897
+ renderModel: defaultModelRenderer,
28898
+ run: async (args, context) => {
28899
+ assertObject(args, "args");
28900
+ const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
28901
+ const thirdParty = args.thirdPartyOnly === true;
28902
+ const result = await runAdb(context, `${adbPrefix(serial)} shell pm list packages${thirdParty ? " -3" : ""}`, 30000);
28903
+ if (adbUnavailable(result))
28904
+ throw new Error("adb is not available in the container");
28905
+ const filter = typeof args.filter === "string" ? args.filter.trim().toLowerCase() : "";
28906
+ let packages = result.stdout.split(`
28907
+ `).map((line) => line.replace(/^package:/, "").trim()).filter(Boolean);
28908
+ if (filter)
28909
+ packages = packages.filter((name) => name.toLowerCase().includes(filter));
28910
+ packages.sort();
28911
+ return {
28912
+ ok: result.exitCode === 0,
28913
+ summary: `${packages.length} package(s)${thirdParty ? " (third-party)" : ""}${filter ? ` matching "${filter}"` : ""}`,
28914
+ output: packages.length ? packages.join(`
28915
+ `) : "no packages matched",
28916
+ metadata: {
28917
+ serial,
28918
+ count: packages.length,
28919
+ packages: packages.slice(0, 1000)
28920
+ }
28921
+ };
28922
+ }
28923
+ };
28924
+ androidDeviceInfoTool = {
28925
+ name: "android_device_info",
28926
+ description: "Summarize the target device in one call: android version, sdk, model, cpu abi, and root/su availability. Use this early to shape the methodology for the device.",
28927
+ inputSchema: {
28928
+ type: "object",
28929
+ properties: {
28930
+ serial: SERIAL_PROP
28931
+ },
28932
+ additionalProperties: false
28933
+ },
28934
+ mutates: false,
28935
+ timeoutMs: 30000,
28936
+ parallel: true,
28937
+ renderHuman: defaultHumanRenderer,
28938
+ renderModel: defaultModelRenderer,
28939
+ run: async (args, context) => {
28940
+ assertObject(args, "args");
28941
+ const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
28942
+ const props = ["ro.build.version.release", "ro.build.version.sdk", "ro.product.model", "ro.product.manufacturer", "ro.product.cpu.abi", "ro.build.type"];
28943
+ const command = `${adbPrefix(serial)} shell ${shellQuote7(`for p in ${props.join(" ")}; do echo "$p=$(getprop $p)"; done; echo su=$(command -v su || echo none)`)}`;
28944
+ const result = await runAdb(context, command, 30000);
28945
+ if (adbUnavailable(result))
28946
+ throw new Error("adb is not available in the container");
28947
+ const info = {};
28948
+ for (const line of result.stdout.split(`
28949
+ `)) {
28950
+ const eq = line.indexOf("=");
28951
+ if (eq > 0)
28952
+ info[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
28953
+ }
28954
+ return {
28955
+ ok: result.exitCode === 0,
28956
+ summary: `${info["ro.product.manufacturer"] ?? "?"} ${info["ro.product.model"] ?? serial}, android ${info["ro.build.version.release"] ?? "?"} (sdk ${info["ro.build.version.sdk"] ?? "?"})`,
28957
+ output: Object.entries(info).map(([key, value]) => `${key}: ${value}`).join(`
28958
+ `) || result.stdout,
28959
+ metadata: {
28960
+ serial,
28961
+ info
28962
+ }
28963
+ };
28964
+ }
28965
+ };
28966
+ androidLogcatTool = {
28967
+ name: "android_logcat",
28968
+ description: "Capture recent logcat output from the device, optionally filtered by tag. Use this to spot leaked tokens, stack traces, and app behavior after an action; it dumps the current buffer and returns.",
28969
+ inputSchema: {
28970
+ type: "object",
28971
+ properties: {
28972
+ tag: {
28973
+ type: "string",
28974
+ description: "logcat tag filter; only lines from this tag are returned"
28975
+ },
28976
+ lines: {
28977
+ type: "integer",
28978
+ minimum: 1,
28979
+ maximum: 5000,
28980
+ description: "maximum trailing lines to return (default 200)"
28981
+ },
28982
+ serial: SERIAL_PROP
28983
+ },
28984
+ additionalProperties: false
28985
+ },
28986
+ mutates: false,
28987
+ timeoutMs: 30000,
28988
+ parallel: true,
28989
+ renderHuman: defaultHumanRenderer,
28990
+ renderModel: defaultModelRenderer,
28991
+ run: async (args, context) => {
28992
+ assertObject(args, "args");
28993
+ const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
28994
+ const lines = typeof args.lines === "number" && Number.isInteger(args.lines) ? Math.max(1, Math.min(5000, args.lines)) : 200;
28995
+ const tag = typeof args.tag === "string" && args.tag.trim() ? args.tag.trim() : "";
28996
+ const filter = tag ? ` -s ${shellQuote7(tag)}` : "";
28997
+ const result = await runAdb(context, `${adbPrefix(serial)} logcat -d -t ${lines}${filter}`, 30000);
28998
+ if (adbUnavailable(result))
28999
+ throw new Error("adb is not available in the container");
29000
+ const output = result.stdout.trim();
29001
+ return {
29002
+ ok: result.exitCode === 0,
29003
+ summary: `${output.split(`
29004
+ `).filter(Boolean).length} logcat line(s)${tag ? ` for tag ${tag}` : ""}`,
29005
+ output: output || "(empty logcat buffer)",
29006
+ metadata: {
29007
+ serial,
29008
+ tag: tag || null
29009
+ }
29010
+ };
29011
+ }
29012
+ };
29013
+ });
29014
+
29015
+ // src/agent-tools/android/app.ts
29016
+ function sanitizePackage(value) {
29017
+ const clean = value.trim();
29018
+ if (!/^[a-zA-Z][a-zA-Z0-9_.]*$/.test(clean))
29019
+ throw new Error("package must be a valid android package name");
29020
+ return clean;
29021
+ }
29022
+ function appLifecycleTool(name, verb) {
29023
+ return {
29024
+ name,
29025
+ description: verb === "start" ? "Launch an app by package name using monkey so the default launcher activity starts. Use before ui or dynamic tools that need the app running." : "Force-stop an app by package name. Use to reset app state between tests.",
29026
+ inputSchema: {
29027
+ type: "object",
29028
+ required: ["package"],
29029
+ properties: {
29030
+ package: {
29031
+ type: "string",
29032
+ description: "installed package name to control"
29033
+ },
29034
+ serial: SERIAL_PROP2
29035
+ },
29036
+ additionalProperties: false
29037
+ },
29038
+ mutates: true,
29039
+ timeoutMs: 30000,
29040
+ parallel: false,
29041
+ renderHuman: defaultHumanRenderer,
29042
+ renderModel: defaultModelRenderer,
29043
+ run: async (args, context) => {
29044
+ assertObject(args, "args");
29045
+ const pkg = sanitizePackage(asString(args.package, "package"));
29046
+ const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
29047
+ const shell = verb === "start" ? `monkey -p ${shellQuote7(pkg)} -c android.intent.category.LAUNCHER 1` : `am force-stop ${shellQuote7(pkg)}`;
29048
+ const result = await runAdb(context, `${adbPrefix(serial)} shell ${shellQuote7(shell)}`, 30000);
29049
+ if (adbUnavailable(result))
29050
+ throw new Error("adb is not available in the container");
29051
+ const output = `${result.stdout}${result.stderr ? `
29052
+ ${result.stderr}` : ""}`.trim();
29053
+ const ok = result.exitCode === 0 && !/error|no activities found/i.test(output);
29054
+ return {
29055
+ ok,
29056
+ summary: ok ? `${verb === "start" ? "started" : "stopped"} ${pkg}` : `could not ${verb} ${pkg}`,
29057
+ output: output || "(no output)",
29058
+ metadata: {
29059
+ serial,
29060
+ package: pkg
29061
+ }
29062
+ };
29063
+ }
29064
+ };
29065
+ }
29066
+ var SERIAL_PROP2, androidApkPullTool, androidInstallTool, androidAppStartTool, androidAppStopTool, androidDeeplinkTool, androidPullFileTool;
29067
+ var init_app = __esm(() => {
29068
+ init_renderers();
29069
+ init_shared4();
29070
+ SERIAL_PROP2 = {
29071
+ type: "string",
29072
+ description: "device serial from android_devices; omit when exactly one device is connected"
29073
+ };
29074
+ androidApkPullTool = {
29075
+ name: "android_apk_pull",
29076
+ description: "Pull every apk for an installed package (including split apks) from the device into the workspace for static analysis. Returns the local directory and file list.",
29077
+ inputSchema: {
29078
+ type: "object",
29079
+ required: ["package"],
29080
+ properties: {
29081
+ package: {
29082
+ type: "string",
29083
+ description: "installed package name, e.g. com.example.app"
29084
+ },
29085
+ serial: SERIAL_PROP2
29086
+ },
29087
+ additionalProperties: false
29088
+ },
29089
+ mutates: true,
29090
+ timeoutMs: 120000,
29091
+ parallel: false,
29092
+ renderHuman: defaultHumanRenderer,
29093
+ renderModel: defaultModelRenderer,
29094
+ run: async (args, context) => {
29095
+ assertObject(args, "args");
29096
+ const pkg = sanitizePackage(asString(args.package, "package"));
29097
+ const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
29098
+ const pathResult = await runAdb(context, `${adbPrefix(serial)} shell pm path ${shellQuote7(pkg)}`, 30000);
29099
+ if (adbUnavailable(pathResult))
29100
+ throw new Error("adb is not available in the container");
29101
+ const remotes = pathResult.stdout.split(`
29102
+ `).map((line) => line.replace(/^package:/, "").trim()).filter(Boolean);
29103
+ if (remotes.length === 0)
29104
+ throw new Error(`package not found on device: ${pkg}`);
29105
+ const destDir = `android/${pkg}`;
29106
+ await runAdb(context, `mkdir -p ${shellQuote7(destDir)}`, 1e4);
29107
+ const pulled = [];
29108
+ const errors = [];
29109
+ for (const remote of remotes) {
29110
+ const local = `${destDir}/${remote.split("/").pop() || "base.apk"}`;
29111
+ const result = await runAdb(context, `${adbPrefix(serial)} pull ${shellQuote7(remote)} ${shellQuote7(local)}`, 90000);
29112
+ if (result.exitCode === 0)
29113
+ pulled.push(local);
29114
+ else
29115
+ errors.push(`${remote}: ${compactError2(result.stderr || result.stdout)}`);
29116
+ }
29117
+ return {
29118
+ ok: pulled.length > 0,
29119
+ summary: pulled.length ? `pulled ${pulled.length} apk(s) for ${pkg} to ${destDir}` : `failed to pull apks for ${pkg}`,
29120
+ output: [...pulled.map((path) => `pulled: ${path}`), ...errors.map((err) => `error: ${err}`)].join(`
29121
+ `),
29122
+ metadata: {
29123
+ serial,
29124
+ package: pkg,
29125
+ directory: destDir,
29126
+ files: pulled
29127
+ }
29128
+ };
29129
+ }
29130
+ };
29131
+ androidInstallTool = {
29132
+ name: "android_install",
29133
+ description: "Install an apk on the device with adb install -r. Use for patched or instrumentation builds; the path is a workspace-relative apk file.",
29134
+ inputSchema: {
29135
+ type: "object",
29136
+ required: ["apkPath"],
29137
+ properties: {
29138
+ apkPath: {
29139
+ type: "string",
29140
+ description: "workspace-relative path to the apk file to install"
29141
+ },
29142
+ serial: SERIAL_PROP2
29143
+ },
29144
+ additionalProperties: false
29145
+ },
29146
+ mutates: true,
29147
+ timeoutMs: 120000,
29148
+ parallel: false,
29149
+ renderHuman: defaultHumanRenderer,
29150
+ renderModel: defaultModelRenderer,
29151
+ run: async (args, context) => {
29152
+ assertObject(args, "args");
29153
+ const apkPath = asString(args.apkPath, "apkPath").trim();
29154
+ const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
29155
+ const result = await runAdb(context, `${adbPrefix(serial)} install -r ${shellQuote7(apkPath)}`, 120000);
29156
+ if (adbUnavailable(result))
29157
+ throw new Error("adb is not available in the container");
29158
+ const text2 = `${result.stdout}${result.stderr}`.trim();
29159
+ const ok = /success/i.test(text2);
29160
+ return {
29161
+ ok,
29162
+ summary: ok ? `installed ${apkPath} on ${serial}` : `install failed for ${apkPath}`,
29163
+ output: text2 || `exit ${result.exitCode}`,
29164
+ metadata: {
29165
+ serial,
29166
+ apkPath,
29167
+ installed: ok
29168
+ }
29169
+ };
29170
+ }
29171
+ };
29172
+ androidAppStartTool = appLifecycleTool("android_app_start", "start");
29173
+ androidAppStopTool = appLifecycleTool("android_app_stop", "stop");
29174
+ androidDeeplinkTool = {
29175
+ name: "android_deeplink",
29176
+ description: "Fire a deep-link intent (VIEW) on the device to test deep-link and exported-component handling. Optionally scope it to a package to target one app.",
29177
+ inputSchema: {
29178
+ type: "object",
29179
+ required: ["uri"],
29180
+ properties: {
29181
+ uri: {
29182
+ type: "string",
29183
+ description: "deep link uri to open, e.g. myapp://path?arg=1"
29184
+ },
29185
+ package: {
29186
+ type: "string",
29187
+ description: "optional package to constrain the intent to one app"
29188
+ },
29189
+ serial: SERIAL_PROP2
29190
+ },
29191
+ additionalProperties: false
29192
+ },
29193
+ mutates: true,
29194
+ timeoutMs: 30000,
29195
+ parallel: false,
29196
+ renderHuman: defaultHumanRenderer,
29197
+ renderModel: defaultModelRenderer,
29198
+ run: async (args, context) => {
29199
+ assertObject(args, "args");
29200
+ const uri = asString(args.uri, "uri").trim();
29201
+ const pkg = typeof args.package === "string" && args.package.trim() ? sanitizePackage(args.package) : "";
29202
+ const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
29203
+ const shell = `am start -a android.intent.action.VIEW -d ${shellQuote7(uri)}${pkg ? ` ${shellQuote7(pkg)}` : ""}`;
29204
+ const result = await runAdb(context, `${adbPrefix(serial)} shell ${shellQuote7(shell)}`, 30000);
29205
+ if (adbUnavailable(result))
29206
+ throw new Error("adb is not available in the container");
29207
+ const output = `${result.stdout}${result.stderr ? `
29208
+ ${result.stderr}` : ""}`.trim();
29209
+ const ok = result.exitCode === 0 && !/error|exception/i.test(output);
29210
+ return {
29211
+ ok,
29212
+ summary: ok ? `fired deep link ${uri}` : `deep link may have failed: ${uri}`,
29213
+ output: output || "(no output)",
29214
+ metadata: {
29215
+ serial,
29216
+ uri,
29217
+ package: pkg || null
29218
+ }
29219
+ };
29220
+ }
29221
+ };
29222
+ androidPullFileTool = {
29223
+ name: "android_pull_file",
29224
+ description: "Read a file from the device. When package is given, uses run-as <package> to reach app-private files (requires a debuggable app). Returns bounded file contents.",
29225
+ inputSchema: {
29226
+ type: "object",
29227
+ required: ["path"],
29228
+ properties: {
29229
+ path: {
29230
+ type: "string",
29231
+ description: "absolute device path to read, e.g. /data/data/pkg/shared_prefs/x.xml"
29232
+ },
29233
+ package: {
29234
+ type: "string",
29235
+ description: "package to read app-private files via run-as; omit for world-readable paths"
29236
+ },
29237
+ serial: SERIAL_PROP2
29238
+ },
29239
+ additionalProperties: false
29240
+ },
29241
+ mutates: false,
29242
+ timeoutMs: 30000,
29243
+ parallel: true,
29244
+ renderHuman: defaultHumanRenderer,
29245
+ renderModel: defaultModelRenderer,
29246
+ run: async (args, context) => {
29247
+ assertObject(args, "args");
29248
+ const path = asString(args.path, "path").trim();
29249
+ const pkg = typeof args.package === "string" && args.package.trim() ? sanitizePackage(args.package) : "";
29250
+ const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
29251
+ const shell = pkg ? `run-as ${shellQuote7(pkg)} cat ${shellQuote7(path)}` : `cat ${shellQuote7(path)}`;
29252
+ const result = await runAdb(context, `${adbPrefix(serial)} shell ${shellQuote7(shell)}`, 30000, 4000000);
29253
+ if (adbUnavailable(result))
29254
+ throw new Error("adb is not available in the container");
29255
+ const ok = result.exitCode === 0 && !/no such file|permission denied|not debuggable|run-as:/i.test(result.stderr);
29256
+ return {
29257
+ ok,
29258
+ summary: ok ? `read ${path}` : `could not read ${path}`,
29259
+ output: ok ? result.stdout : `${result.stdout}${result.stderr}`.trim() || "(no output)",
29260
+ metadata: {
29261
+ serial,
29262
+ path,
29263
+ package: pkg || null
29264
+ }
29265
+ };
29266
+ }
29267
+ };
29268
+ });
29269
+
29270
+ // src/agent-tools/android/static.ts
29271
+ function quoteDir(value) {
29272
+ const clean = value.trim();
29273
+ if (!clean || clean.includes(".."))
29274
+ throw new Error("directory must be a workspace-relative path without ..");
29275
+ return clean;
29276
+ }
29277
+ async function readManifest(context, dir) {
29278
+ const result = await backend(context).exec(`cat ${shellQuote7(`${dir}/AndroidManifest.xml`)}`, 20000, context.signal, 4000000);
29279
+ if (result.exitCode !== 0)
29280
+ throw new Error(`could not read AndroidManifest.xml in ${dir}; decompile with android_decompile first`);
29281
+ return result.stdout;
29282
+ }
29283
+ var DANGEROUS_PERMISSIONS, SECRET_PATTERN, androidDecompileTool, androidManifestTool, androidPermissionsTool, androidExportedComponentsTool, androidScanSecretsTool, androidGrepApkTool;
29284
+ var init_static = __esm(() => {
29285
+ init_backend();
29286
+ init_renderers();
29287
+ init_shared4();
29288
+ DANGEROUS_PERMISSIONS = new Set(["android.permission.READ_SMS", "android.permission.SEND_SMS", "android.permission.RECEIVE_SMS", "android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS", "android.permission.ACCESS_FINE_LOCATION", "android.permission.ACCESS_COARSE_LOCATION", "android.permission.ACCESS_BACKGROUND_LOCATION", "android.permission.RECORD_AUDIO", "android.permission.CAMERA", "android.permission.READ_EXTERNAL_STORAGE", "android.permission.WRITE_EXTERNAL_STORAGE", "android.permission.MANAGE_EXTERNAL_STORAGE", "android.permission.READ_PHONE_STATE", "android.permission.READ_CALL_LOG", "android.permission.WRITE_CALL_LOG", "android.permission.REQUEST_INSTALL_PACKAGES", "android.permission.SYSTEM_ALERT_WINDOW", "android.permission.QUERY_ALL_PACKAGES", "android.permission.WRITE_SETTINGS"]);
29289
+ SECRET_PATTERN = ["AKIA[0-9A-Z]{16}", "AIza[0-9A-Za-z_-]{35}", "-----BEGIN [A-Z ]*PRIVATE KEY-----", "eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}", `(api[_-]?key|secret|passwd|password|token|bearer)["'\\s:=]{1,4}[A-Za-z0-9_.-]{8,}`, "https://[a-z0-9-]+\\.firebaseio\\.com"].join("|");
29290
+ androidDecompileTool = {
29291
+ name: "android_decompile",
29292
+ description: "Decompile an apk with apktool into smali, decoded resources, and a readable AndroidManifest.xml. Returns the output directory to pass to the other android static tools.",
29293
+ inputSchema: {
29294
+ type: "object",
29295
+ required: ["apkPath"],
29296
+ properties: {
29297
+ apkPath: {
29298
+ type: "string",
29299
+ description: "workspace-relative path to the apk to decompile"
29300
+ }
29301
+ },
29302
+ additionalProperties: false
29303
+ },
29304
+ mutates: true,
29305
+ timeoutMs: 240000,
29306
+ parallel: false,
29307
+ renderHuman: defaultHumanRenderer,
29308
+ renderModel: defaultModelRenderer,
29309
+ run: async (args, context) => {
29310
+ assertObject(args, "args");
29311
+ const apkPath = asString(args.apkPath, "apkPath").trim();
29312
+ const base = apkPath.split("/").pop()?.replace(/\.apk$/i, "") || "app";
29313
+ const outDir = `android/decompiled/${base}`;
29314
+ const result = await backend(context).exec(`apktool d -f -o ${shellQuote7(outDir)} ${shellQuote7(apkPath)}`, 240000, context.signal, 2000000);
29315
+ if (result.exitCode === 127)
29316
+ throw new Error("apktool is not available in the container");
29317
+ const ok = result.exitCode === 0;
29318
+ return {
29319
+ ok,
29320
+ summary: ok ? `decompiled to ${outDir}` : `apktool failed on ${apkPath}`,
29321
+ output: ok ? `output directory: ${outDir}
29322
+ ${result.stdout}`.trim() : compactError2(result.stderr || result.stdout),
29323
+ metadata: {
29324
+ apkPath,
29325
+ directory: outDir
29326
+ }
29327
+ };
29328
+ }
29329
+ };
29330
+ androidManifestTool = {
29331
+ name: "android_manifest",
29332
+ description: "Read and summarize AndroidManifest.xml from a decompiled apk directory: package, sdk versions, debuggable/allowBackup flags, and permission and component counts.",
29333
+ inputSchema: {
29334
+ type: "object",
29335
+ required: ["directory"],
29336
+ properties: {
29337
+ directory: {
29338
+ type: "string",
29339
+ description: "decompiled apk directory from android_decompile"
29340
+ }
29341
+ },
29342
+ additionalProperties: false
29343
+ },
29344
+ mutates: false,
29345
+ timeoutMs: 30000,
29346
+ parallel: true,
29347
+ renderHuman: defaultHumanRenderer,
29348
+ renderModel: defaultModelRenderer,
29349
+ run: async (args, context) => {
29350
+ assertObject(args, "args");
29351
+ const dir = quoteDir(asString(args.directory, "directory"));
29352
+ const xml = await readManifest(context, dir);
29353
+ const pkg = /package="([^"]+)"/.exec(xml)?.[1] ?? "unknown";
29354
+ const debuggable = /android:debuggable="true"/.test(xml);
29355
+ const allowBackup = !/android:allowBackup="false"/.test(xml);
29356
+ const permissions = [...xml.matchAll(/<uses-permission[^>]*android:name="([^"]+)"/g)].map((match) => match[1]);
29357
+ const counts = {
29358
+ activities: (xml.match(/<activity[\s>]/g) ?? []).length,
29359
+ services: (xml.match(/<service[\s>]/g) ?? []).length,
29360
+ receivers: (xml.match(/<receiver[\s>]/g) ?? []).length,
29361
+ providers: (xml.match(/<provider[\s>]/g) ?? []).length
29362
+ };
29363
+ const output = [`package: ${pkg}`, `debuggable: ${debuggable}`, `allowBackup: ${allowBackup}`, `permissions: ${permissions.length}`, `components: activity=${counts.activities} service=${counts.services} receiver=${counts.receivers} provider=${counts.providers}`].join(`
29364
+ `);
29365
+ return {
29366
+ ok: true,
29367
+ summary: `${pkg}${debuggable ? " [debuggable]" : ""}${allowBackup ? " [allowBackup]" : ""}`,
29368
+ output,
29369
+ metadata: {
29370
+ package: pkg,
29371
+ debuggable,
29372
+ allowBackup,
29373
+ permissions,
29374
+ counts
29375
+ }
29376
+ };
29377
+ }
29378
+ };
29379
+ androidPermissionsTool = {
29380
+ name: "android_permissions",
29381
+ description: "List declared permissions from a decompiled apk and flag dangerous ones (sms, contacts, location, storage, install-packages, overlay, etc).",
29382
+ inputSchema: {
29383
+ type: "object",
29384
+ required: ["directory"],
29385
+ properties: {
29386
+ directory: {
29387
+ type: "string",
29388
+ description: "decompiled apk directory from android_decompile"
29389
+ }
29390
+ },
29391
+ additionalProperties: false
29392
+ },
29393
+ mutates: false,
29394
+ timeoutMs: 30000,
29395
+ parallel: true,
29396
+ renderHuman: defaultHumanRenderer,
29397
+ renderModel: defaultModelRenderer,
29398
+ run: async (args, context) => {
29399
+ assertObject(args, "args");
29400
+ const dir = quoteDir(asString(args.directory, "directory"));
29401
+ const xml = await readManifest(context, dir);
29402
+ const permissions = [...xml.matchAll(/<uses-permission[^>]*android:name="([^"]+)"/g)].map((match) => match[1]).sort();
29403
+ const dangerous = permissions.filter((name) => DANGEROUS_PERMISSIONS.has(name));
29404
+ const output = permissions.length ? permissions.map((name) => `${dangerous.includes(name) ? "[!] " : " "}${name}`).join(`
29405
+ `) : "no permissions declared";
29406
+ return {
29407
+ ok: true,
29408
+ summary: `${permissions.length} permission(s), ${dangerous.length} dangerous`,
29409
+ output,
29410
+ metadata: {
29411
+ permissions,
29412
+ dangerous
29413
+ }
29414
+ };
29415
+ }
29416
+ };
29417
+ androidExportedComponentsTool = {
29418
+ name: "android_exported_components",
29419
+ description: "List exported components (activity, service, receiver, provider) from a decompiled apk. Exported components are reachable by other apps and are a primary attack surface.",
29420
+ inputSchema: {
29421
+ type: "object",
29422
+ required: ["directory"],
29423
+ properties: {
29424
+ directory: {
29425
+ type: "string",
29426
+ description: "decompiled apk directory from android_decompile"
29427
+ }
29428
+ },
29429
+ additionalProperties: false
29430
+ },
29431
+ mutates: false,
29432
+ timeoutMs: 30000,
29433
+ parallel: true,
29434
+ renderHuman: defaultHumanRenderer,
29435
+ renderModel: defaultModelRenderer,
29436
+ run: async (args, context) => {
29437
+ assertObject(args, "args");
29438
+ const dir = quoteDir(asString(args.directory, "directory"));
29439
+ const xml = await readManifest(context, dir);
29440
+ const exported = [];
29441
+ for (const kind of ["activity", "activity-alias", "service", "receiver", "provider"]) {
29442
+ const regex = new RegExp(`<${kind}\\b[^>]*?(?:/>|>[\\s\\S]*?</${kind}>)`, "g");
29443
+ for (const match of xml.matchAll(regex)) {
29444
+ const block = match[0];
29445
+ const name = /android:name="([^"]+)"/.exec(block)?.[1] ?? "(unknown)";
29446
+ const explicitExport = /android:exported="true"/.test(block);
29447
+ const implicitExport = !/android:exported="false"/.test(block) && /<intent-filter/.test(block);
29448
+ if (explicitExport || implicitExport)
29449
+ exported.push({
29450
+ kind,
29451
+ name,
29452
+ explicit: explicitExport
29453
+ });
29454
+ }
29455
+ }
29456
+ const output = exported.length ? exported.map((item) => `${item.kind} ${item.explicit ? "exported=true" : "intent-filter"} ${item.name}`).join(`
29457
+ `) : "no exported components found";
29458
+ return {
29459
+ ok: true,
29460
+ summary: `${exported.length} exported component(s)`,
29461
+ output,
29462
+ metadata: {
29463
+ exported
29464
+ }
29465
+ };
29466
+ }
29467
+ };
29468
+ androidScanSecretsTool = {
29469
+ name: "android_scan_secrets",
29470
+ description: "Scan a decompiled apk directory for hardcoded secrets: aws/google api keys, private keys, jwts, and password/token assignments. Matches are candidate leads, not confirmed findings.",
29471
+ inputSchema: {
29472
+ type: "object",
29473
+ required: ["directory"],
29474
+ properties: {
29475
+ directory: {
29476
+ type: "string",
29477
+ description: "decompiled apk directory from android_decompile"
29478
+ },
29479
+ limit: {
29480
+ type: "integer",
29481
+ minimum: 1,
29482
+ maximum: 1000,
29483
+ description: "maximum matching lines to return (default 200)"
29484
+ }
29485
+ },
29486
+ additionalProperties: false
29487
+ },
29488
+ mutates: false,
29489
+ timeoutMs: 90000,
29490
+ parallel: true,
29491
+ renderHuman: defaultHumanRenderer,
29492
+ renderModel: defaultModelRenderer,
29493
+ run: async (args, context) => {
29494
+ assertObject(args, "args");
29495
+ const dir = quoteDir(asString(args.directory, "directory"));
29496
+ const limit = typeof args.limit === "number" && Number.isInteger(args.limit) ? Math.max(1, Math.min(1000, args.limit)) : 200;
29497
+ const command = `grep -rEIn ${shellQuote7(SECRET_PATTERN)} ${shellQuote7(dir)} 2>/dev/null | head -n ${limit}`;
29498
+ const result = await backend(context).exec(command, 90000, context.signal, 2000000);
29499
+ const lines = result.stdout.split(`
29500
+ `).map((line) => line.trim()).filter(Boolean);
29501
+ return {
29502
+ ok: true,
29503
+ summary: lines.length ? `${lines.length} secret candidate line(s)` : "no secret candidates matched",
29504
+ output: lines.length ? lines.join(`
29505
+ `) : "no matches",
29506
+ metadata: {
29507
+ directory: dir,
29508
+ matches: lines.length,
29509
+ truncated: lines.length >= limit
29510
+ }
29511
+ };
29512
+ }
29513
+ };
29514
+ androidGrepApkTool = {
29515
+ name: "android_grep_apk",
29516
+ description: "Regex-search a decompiled apk directory (smali, resources, assets). Use for urls, class names, string constants, and crypto usage after android_decompile.",
29517
+ inputSchema: {
29518
+ type: "object",
29519
+ required: ["directory", "pattern"],
29520
+ properties: {
29521
+ directory: {
29522
+ type: "string",
29523
+ description: "decompiled apk directory from android_decompile"
29524
+ },
29525
+ pattern: {
29526
+ type: "string",
29527
+ description: "extended regular expression to search for"
29528
+ },
29529
+ limit: {
29530
+ type: "integer",
29531
+ minimum: 1,
29532
+ maximum: 1000,
29533
+ description: "maximum matching lines to return (default 200)"
29534
+ }
29535
+ },
29536
+ additionalProperties: false
29537
+ },
29538
+ mutates: false,
29539
+ timeoutMs: 90000,
29540
+ parallel: true,
29541
+ renderHuman: defaultHumanRenderer,
29542
+ renderModel: defaultModelRenderer,
29543
+ run: async (args, context) => {
29544
+ assertObject(args, "args");
29545
+ const dir = quoteDir(asString(args.directory, "directory"));
29546
+ const pattern = asString(args.pattern, "pattern");
29547
+ const limit = typeof args.limit === "number" && Number.isInteger(args.limit) ? Math.max(1, Math.min(1000, args.limit)) : 200;
29548
+ const command = `grep -rEIn ${shellQuote7(pattern)} ${shellQuote7(dir)} 2>/dev/null | head -n ${limit}`;
29549
+ const result = await backend(context).exec(command, 90000, context.signal, 2000000);
29550
+ const lines = result.stdout.split(`
29551
+ `).map((line) => line.trim()).filter(Boolean);
29552
+ return {
29553
+ ok: true,
29554
+ summary: lines.length ? `${lines.length} match(es) for /${pattern}/` : `no matches for /${pattern}/`,
29555
+ output: lines.length ? lines.join(`
29556
+ `) : "no matches",
29557
+ metadata: {
29558
+ directory: dir,
29559
+ pattern,
29560
+ matches: lines.length,
29561
+ truncated: lines.length >= limit
29562
+ }
29563
+ };
29564
+ }
29565
+ };
29566
+ });
29567
+
29568
+ // src/agent-tools/android/ui.ts
29569
+ function parseUiNodes(xml) {
29570
+ const nodes = [];
29571
+ for (const match of xml.matchAll(/<node\b([^>]*)>/g)) {
29572
+ const attrs = match[1] ?? "";
29573
+ const bounds = /bounds="\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]"/.exec(attrs);
29574
+ if (!bounds)
29575
+ continue;
29576
+ const x1 = Number(bounds[1]);
29577
+ const y1 = Number(bounds[2]);
29578
+ const x2 = Number(bounds[3]);
29579
+ const y2 = Number(bounds[4]);
29580
+ const attr = (name) => new RegExp(`\\b${name}="([^"]*)"`).exec(attrs)?.[1] ?? "";
29581
+ nodes.push({
29582
+ text: attr("text"),
29583
+ resourceId: attr("resource-id"),
29584
+ contentDesc: attr("content-desc"),
29585
+ className: attr("class"),
29586
+ clickable: attr("clickable") === "true",
29587
+ bounds: [x1, y1, x2, y2],
29588
+ center: [Math.round((x1 + x2) / 2), Math.round((y1 + y2) / 2)]
29589
+ });
29590
+ }
29591
+ return nodes;
29592
+ }
29593
+ function findUiNode(nodes, selector) {
29594
+ const wantId = selector.resourceId?.trim();
29595
+ const wantText = selector.text?.trim();
29596
+ const wantDesc = selector.contentDesc?.trim();
29597
+ return nodes.find((node) => {
29598
+ if (wantId && !(node.resourceId === wantId || node.resourceId.endsWith(`/${wantId}`)))
29599
+ return false;
29600
+ if (wantText && !(node.text === wantText || node.text.toLowerCase().includes(wantText.toLowerCase())))
29601
+ return false;
29602
+ if (wantDesc && !(node.contentDesc === wantDesc || node.contentDesc.toLowerCase().includes(wantDesc.toLowerCase())))
29603
+ return false;
29604
+ return Boolean(wantId || wantText || wantDesc);
29605
+ });
29606
+ }
29607
+ async function dumpHierarchy(context, serial) {
29608
+ const remote = "/sdcard/farai_uidump.xml";
29609
+ const dump = await runAdb(context, `${adbPrefix(serial)} shell uiautomator dump ${remote}`, 30000);
29610
+ if (adbUnavailable(dump))
29611
+ throw new Error("adb is not available in the container");
29612
+ const read = await runAdb(context, `${adbPrefix(serial)} shell cat ${remote}`, 20000, 8000000);
29613
+ const xml = read.stdout.trim();
29614
+ if (!xml.includes("<hierarchy") && !xml.includes("<node")) {
29615
+ throw new Error(`could not capture ui hierarchy: ${(dump.stderr || dump.stdout || "no output").trim().slice(0, 200)}`);
29616
+ }
29617
+ return xml;
29618
+ }
29619
+ function describeNode(node) {
29620
+ const label = node.text || node.contentDesc || node.resourceId || node.className;
29621
+ const parts = [node.resourceId ? `id=${node.resourceId}` : "", node.text ? `text=${JSON.stringify(node.text)}` : "", node.contentDesc ? `desc=${JSON.stringify(node.contentDesc)}` : "", node.clickable ? "clickable" : "", `@${node.center[0]},${node.center[1]}`].filter(Boolean);
29622
+ return `${label} \u2014 ${parts.join(" ")}`;
29623
+ }
29624
+ var SERIAL_PROP3, androidUiDumpTool, androidUiHierarchyTool, androidScreenshotTool, androidUiTapTool, androidUiTapElementTool, androidUiTypeTool, androidUiSwipeTool, KEYEVENTS, androidUiKeyTool, androidUiWindowSizeTool, androidUiWaitForTool;
29625
+ var init_ui = __esm(() => {
29626
+ init_renderers();
29627
+ init_shared4();
29628
+ SERIAL_PROP3 = {
29629
+ type: "string",
29630
+ description: "device serial from android_devices; omit when exactly one device is connected"
29631
+ };
29632
+ androidUiDumpTool = {
29633
+ name: "android_ui_dump",
29634
+ description: "Capture the current screen's ui hierarchy via uiautomator and return interactive elements (text, resource-id, content-desc, clickable, tap center). Use this before android_ui_tap_element to see what is on screen.",
29635
+ inputSchema: {
29636
+ type: "object",
29637
+ properties: {
29638
+ clickableOnly: {
29639
+ type: "boolean",
29640
+ description: "return only clickable elements when true (default true)"
29641
+ },
29642
+ serial: SERIAL_PROP3
29643
+ },
29644
+ additionalProperties: false
29645
+ },
29646
+ mutates: false,
29647
+ timeoutMs: 40000,
29648
+ parallel: false,
29649
+ renderHuman: defaultHumanRenderer,
29650
+ renderModel: defaultModelRenderer,
29651
+ run: async (args, context) => {
29652
+ assertObject(args, "args");
29653
+ const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
29654
+ const xml = await dumpHierarchy(context, serial);
29655
+ const all = parseUiNodes(xml);
29656
+ const clickableOnly = args.clickableOnly !== false;
29657
+ const shown = (clickableOnly ? all.filter((node) => node.clickable) : all).filter((node) => node.text || node.contentDesc || node.resourceId);
29658
+ return {
29659
+ ok: true,
29660
+ summary: `${shown.length} element(s) of ${all.length} on screen`,
29661
+ output: shown.length ? shown.map(describeNode).join(`
29662
+ `) : "no labeled elements found",
29663
+ metadata: {
29664
+ serial,
29665
+ total: all.length,
29666
+ elements: shown.slice(0, 200)
29667
+ }
29668
+ };
29669
+ }
29670
+ };
29671
+ androidUiHierarchyTool = {
29672
+ name: "android_ui_hierarchy",
29673
+ description: "Return the full raw uiautomator xml hierarchy of the current screen. Use when android_ui_dump omits an element you need to inspect precisely.",
29674
+ inputSchema: {
29675
+ type: "object",
29676
+ properties: {
29677
+ serial: SERIAL_PROP3
29678
+ },
29679
+ additionalProperties: false
29680
+ },
29681
+ mutates: false,
29682
+ timeoutMs: 40000,
29683
+ parallel: false,
29684
+ renderHuman: defaultHumanRenderer,
29685
+ renderModel: defaultModelRenderer,
29686
+ run: async (args, context) => {
29687
+ assertObject(args, "args");
29688
+ const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
29689
+ const xml = await dumpHierarchy(context, serial);
29690
+ return {
29691
+ ok: true,
29692
+ summary: `captured ui hierarchy (${xml.length} bytes)`,
29693
+ output: xml,
29694
+ metadata: {
29695
+ serial
29696
+ }
29697
+ };
29698
+ }
29699
+ };
29700
+ androidScreenshotTool = {
29701
+ name: "android_screenshot",
29702
+ description: "Take a screenshot of the current screen and save it as a png in the workspace. Use android's image_view tool afterwards to inspect the saved file.",
29703
+ inputSchema: {
29704
+ type: "object",
29705
+ properties: {
29706
+ serial: SERIAL_PROP3
29707
+ },
29708
+ additionalProperties: false
29709
+ },
29710
+ mutates: false,
29711
+ timeoutMs: 40000,
29712
+ parallel: false,
29713
+ renderHuman: defaultHumanRenderer,
29714
+ renderModel: defaultModelRenderer,
29715
+ run: async (args, context) => {
29716
+ assertObject(args, "args");
29717
+ const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
29718
+ const remote = "/sdcard/farai_screen.png";
29719
+ const local = `android/screenshots/${Date.now()}.png`;
29720
+ await runAdb(context, `mkdir -p android/screenshots`, 1e4);
29721
+ const cap = await runAdb(context, `${adbPrefix(serial)} shell screencap -p ${remote}`, 30000);
29722
+ if (adbUnavailable(cap))
29723
+ throw new Error("adb is not available in the container");
29724
+ const pull = await runAdb(context, `${adbPrefix(serial)} pull ${remote} ${shellQuote7(local)}`, 40000);
29725
+ const ok = pull.exitCode === 0;
29726
+ return {
29727
+ ok,
29728
+ summary: ok ? `saved screenshot to ${local}` : "screenshot capture failed",
29729
+ output: ok ? local : `${pull.stderr || pull.stdout}`.trim() || "screencap failed",
29730
+ metadata: {
29731
+ serial,
29732
+ path: ok ? local : null
29733
+ }
29734
+ };
29735
+ }
29736
+ };
29737
+ androidUiTapTool = {
29738
+ name: "android_ui_tap",
29739
+ description: "Tap the screen at absolute pixel coordinates. Use android_ui_tap_element when you can identify the target by id or text instead.",
29740
+ inputSchema: {
29741
+ type: "object",
29742
+ required: ["x", "y"],
29743
+ properties: {
29744
+ x: {
29745
+ type: "integer",
29746
+ minimum: 0,
29747
+ description: "x pixel coordinate"
29748
+ },
29749
+ y: {
29750
+ type: "integer",
29751
+ minimum: 0,
29752
+ description: "y pixel coordinate"
29753
+ },
29754
+ serial: SERIAL_PROP3
29755
+ },
29756
+ additionalProperties: false
29757
+ },
29758
+ mutates: true,
29759
+ timeoutMs: 20000,
29760
+ parallel: false,
29761
+ renderHuman: defaultHumanRenderer,
29762
+ renderModel: defaultModelRenderer,
29763
+ run: async (args, context) => {
29764
+ assertObject(args, "args");
29765
+ const x = Number(args.x);
29766
+ const y = Number(args.y);
29767
+ if (!Number.isInteger(x) || !Number.isInteger(y))
29768
+ throw new Error("x and y must be integers");
29769
+ const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
29770
+ const result = await runAdb(context, `${adbPrefix(serial)} shell input tap ${x} ${y}`, 20000);
29771
+ if (adbUnavailable(result))
29772
+ throw new Error("adb is not available in the container");
29773
+ return {
29774
+ ok: result.exitCode === 0,
29775
+ summary: `tapped ${x},${y}`,
29776
+ output: result.stdout.trim() || "tapped",
29777
+ metadata: {
29778
+ serial,
29779
+ x,
29780
+ y
29781
+ }
29782
+ };
29783
+ }
29784
+ };
29785
+ androidUiTapElementTool = {
29786
+ name: "android_ui_tap_element",
29787
+ description: "Tap a ui element identified by resource-id, visible text, or content-desc. Dumps the hierarchy, resolves the element center, and taps it. Provide at least one selector.",
29788
+ inputSchema: {
29789
+ type: "object",
29790
+ properties: {
29791
+ resourceId: {
29792
+ type: "string",
29793
+ description: "full or trailing resource-id, e.g. com.app:id/login or login"
29794
+ },
29795
+ text: {
29796
+ type: "string",
29797
+ description: "exact or substring visible text of the element"
29798
+ },
29799
+ contentDesc: {
29800
+ type: "string",
29801
+ description: "exact or substring content-desc of the element"
29802
+ },
29803
+ serial: SERIAL_PROP3
29804
+ },
29805
+ additionalProperties: false
29806
+ },
29807
+ mutates: true,
29808
+ timeoutMs: 40000,
29809
+ parallel: false,
29810
+ renderHuman: defaultHumanRenderer,
29811
+ renderModel: defaultModelRenderer,
29812
+ run: async (args, context) => {
29813
+ assertObject(args, "args");
29814
+ const selector = {
29815
+ ...typeof args.resourceId === "string" ? {
29816
+ resourceId: args.resourceId
29817
+ } : {},
29818
+ ...typeof args.text === "string" ? {
29819
+ text: args.text
29820
+ } : {},
29821
+ ...typeof args.contentDesc === "string" ? {
29822
+ contentDesc: args.contentDesc
29823
+ } : {}
29824
+ };
29825
+ if (!selector.resourceId && !selector.text && !selector.contentDesc)
29826
+ throw new Error("provide at least one of resourceId, text, or contentDesc");
29827
+ const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
29828
+ const xml = await dumpHierarchy(context, serial);
29829
+ const node = findUiNode(parseUiNodes(xml), selector);
29830
+ if (!node)
29831
+ return {
29832
+ ok: false,
29833
+ summary: "no element matched the selector",
29834
+ output: "element not found on the current screen",
29835
+ metadata: {
29836
+ serial,
29837
+ selector
29838
+ }
29839
+ };
29840
+ const [x, y] = node.center;
29841
+ const result = await runAdb(context, `${adbPrefix(serial)} shell input tap ${x} ${y}`, 20000);
29842
+ return {
29843
+ ok: result.exitCode === 0,
29844
+ summary: `tapped ${node.resourceId || node.text || node.contentDesc} @${x},${y}`,
29845
+ output: describeNode(node),
29846
+ metadata: {
29847
+ serial,
29848
+ x,
29849
+ y,
29850
+ node
29851
+ }
29852
+ };
29853
+ }
29854
+ };
29855
+ androidUiTypeTool = {
29856
+ name: "android_ui_type",
29857
+ description: "Type text into the currently focused input field via adb input. Tap the field first with android_ui_tap_element to focus it.",
29858
+ inputSchema: {
29859
+ type: "object",
29860
+ required: ["text"],
29861
+ properties: {
29862
+ text: {
29863
+ type: "string",
29864
+ description: "text to type into the focused field"
29865
+ },
29866
+ serial: SERIAL_PROP3
29867
+ },
29868
+ additionalProperties: false
29869
+ },
29870
+ mutates: true,
29871
+ timeoutMs: 20000,
29872
+ parallel: false,
29873
+ renderHuman: defaultHumanRenderer,
29874
+ renderModel: defaultModelRenderer,
29875
+ run: async (args, context) => {
29876
+ assertObject(args, "args");
29877
+ const text2 = asString(args.text, "text");
29878
+ const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
29879
+ const escaped = text2.replace(/(["\\$`])/g, "\\$1").replace(/ /g, "%s");
29880
+ const result = await runAdb(context, `${adbPrefix(serial)} shell input text ${shellQuote7(escaped)}`, 20000);
29881
+ if (adbUnavailable(result))
29882
+ throw new Error("adb is not available in the container");
29883
+ return {
29884
+ ok: result.exitCode === 0,
29885
+ summary: `typed ${text2.length} char(s)`,
29886
+ output: result.stdout.trim() || "typed",
29887
+ metadata: {
29888
+ serial
29889
+ }
29890
+ };
29891
+ }
29892
+ };
29893
+ androidUiSwipeTool = {
29894
+ name: "android_ui_swipe",
29895
+ description: "Swipe from one point to another over a duration. Use for scrolling, dismissing, and gesture navigation.",
29896
+ inputSchema: {
29897
+ type: "object",
29898
+ required: ["x1", "y1", "x2", "y2"],
29899
+ properties: {
29900
+ x1: {
29901
+ type: "integer",
29902
+ minimum: 0,
29903
+ description: "start x"
29904
+ },
29905
+ y1: {
29906
+ type: "integer",
29907
+ minimum: 0,
29908
+ description: "start y"
29909
+ },
29910
+ x2: {
29911
+ type: "integer",
29912
+ minimum: 0,
29913
+ description: "end x"
29914
+ },
29915
+ y2: {
29916
+ type: "integer",
29917
+ minimum: 0,
29918
+ description: "end y"
29919
+ },
29920
+ durationMs: {
29921
+ type: "integer",
29922
+ minimum: 50,
29923
+ maximum: 1e4,
29924
+ description: "swipe duration in ms (default 300)"
29925
+ },
29926
+ serial: SERIAL_PROP3
29927
+ },
29928
+ additionalProperties: false
29929
+ },
29930
+ mutates: true,
29931
+ timeoutMs: 20000,
29932
+ parallel: false,
29933
+ renderHuman: defaultHumanRenderer,
29934
+ renderModel: defaultModelRenderer,
29935
+ run: async (args, context) => {
29936
+ assertObject(args, "args");
29937
+ const coords = ["x1", "y1", "x2", "y2"].map((key) => Number(args[key]));
29938
+ if (coords.some((value) => !Number.isInteger(value)))
29939
+ throw new Error("x1, y1, x2, y2 must be integers");
29940
+ const duration = typeof args.durationMs === "number" && Number.isInteger(args.durationMs) ? Math.max(50, Math.min(1e4, args.durationMs)) : 300;
29941
+ const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
29942
+ const [x1, y1, x2, y2] = coords;
29943
+ const result = await runAdb(context, `${adbPrefix(serial)} shell input swipe ${x1} ${y1} ${x2} ${y2} ${duration}`, 20000);
29944
+ if (adbUnavailable(result))
29945
+ throw new Error("adb is not available in the container");
29946
+ return {
29947
+ ok: result.exitCode === 0,
29948
+ summary: `swiped ${x1},${y1} -> ${x2},${y2}`,
29949
+ output: result.stdout.trim() || "swiped",
29950
+ metadata: {
29951
+ serial
29952
+ }
29953
+ };
29954
+ }
29955
+ };
29956
+ KEYEVENTS = {
29957
+ back: 4,
29958
+ home: 3,
29959
+ menu: 82,
29960
+ enter: 66,
29961
+ tab: 61,
29962
+ escape: 111,
29963
+ up: 19,
29964
+ down: 20,
29965
+ left: 21,
29966
+ right: 22,
29967
+ delete: 67,
29968
+ search: 84,
29969
+ power: 26,
29970
+ appswitch: 187
29971
+ };
29972
+ androidUiKeyTool = {
29973
+ name: "android_ui_key",
29974
+ description: "Send a keyevent to the device. Accepts a named key (back, home, enter, tab, up, down, delete, ...) or a raw android keycode number.",
29975
+ inputSchema: {
29976
+ type: "object",
29977
+ required: ["key"],
29978
+ properties: {
29979
+ key: {
29980
+ type: "string",
29981
+ description: "named key (back, home, menu, enter, tab, up, down, left, right, delete, search, power, appswitch) or a numeric keycode"
29982
+ },
29983
+ serial: SERIAL_PROP3
29984
+ },
29985
+ additionalProperties: false
29986
+ },
29987
+ mutates: true,
29988
+ timeoutMs: 20000,
29989
+ parallel: false,
29990
+ renderHuman: defaultHumanRenderer,
29991
+ renderModel: defaultModelRenderer,
29992
+ run: async (args, context) => {
29993
+ assertObject(args, "args");
29994
+ const key = asString(args.key, "key").trim().toLowerCase();
29995
+ const code = KEYEVENTS[key] ?? (/^\d+$/.test(key) ? Number(key) : undefined);
29996
+ if (code === undefined)
29997
+ throw new Error(`unknown key "${key}"; use a named key or a numeric keycode`);
29998
+ const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
29999
+ const result = await runAdb(context, `${adbPrefix(serial)} shell input keyevent ${code}`, 20000);
30000
+ if (adbUnavailable(result))
30001
+ throw new Error("adb is not available in the container");
30002
+ return {
30003
+ ok: result.exitCode === 0,
30004
+ summary: `sent key ${key} (${code})`,
30005
+ output: result.stdout.trim() || "sent",
30006
+ metadata: {
30007
+ serial,
30008
+ key,
30009
+ code
30010
+ }
30011
+ };
30012
+ }
30013
+ };
30014
+ androidUiWindowSizeTool = {
30015
+ name: "android_ui_window_size",
30016
+ description: "Return the device screen resolution. Use to compute tap and swipe coordinates.",
30017
+ inputSchema: {
30018
+ type: "object",
30019
+ properties: {
30020
+ serial: SERIAL_PROP3
30021
+ },
30022
+ additionalProperties: false
30023
+ },
30024
+ mutates: false,
30025
+ timeoutMs: 20000,
30026
+ parallel: true,
30027
+ renderHuman: defaultHumanRenderer,
30028
+ renderModel: defaultModelRenderer,
30029
+ run: async (args, context) => {
30030
+ assertObject(args, "args");
30031
+ const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
30032
+ const result = await runAdb(context, `${adbPrefix(serial)} shell wm size`, 20000);
30033
+ if (adbUnavailable(result))
30034
+ throw new Error("adb is not available in the container");
30035
+ const size = /(\d+)x(\d+)/.exec(result.stdout);
30036
+ return {
30037
+ ok: result.exitCode === 0,
30038
+ summary: size ? `${size[1]}x${size[2]}` : result.stdout.trim(),
30039
+ output: result.stdout.trim() || "(no output)",
30040
+ metadata: {
30041
+ serial,
30042
+ ...size ? {
30043
+ width: Number(size[1]),
30044
+ height: Number(size[2])
30045
+ } : {}
30046
+ }
30047
+ };
30048
+ }
30049
+ };
30050
+ androidUiWaitForTool = {
30051
+ name: "android_ui_wait_for",
30052
+ description: "Poll the ui hierarchy until an element matching a selector appears or the timeout elapses. Use after an action that triggers a screen transition.",
30053
+ inputSchema: {
30054
+ type: "object",
30055
+ properties: {
30056
+ resourceId: {
30057
+ type: "string",
30058
+ description: "resource-id to wait for"
30059
+ },
30060
+ text: {
30061
+ type: "string",
30062
+ description: "visible text to wait for (substring match)"
30063
+ },
30064
+ contentDesc: {
30065
+ type: "string",
30066
+ description: "content-desc to wait for (substring match)"
30067
+ },
30068
+ timeoutSeconds: {
30069
+ type: "integer",
30070
+ minimum: 1,
30071
+ maximum: 120,
30072
+ description: "maximum seconds to wait (default 15)"
30073
+ },
30074
+ serial: SERIAL_PROP3
30075
+ },
30076
+ additionalProperties: false
30077
+ },
30078
+ mutates: false,
30079
+ timeoutMs: 130000,
30080
+ parallel: false,
30081
+ renderHuman: defaultHumanRenderer,
30082
+ renderModel: defaultModelRenderer,
30083
+ run: async (args, context) => {
30084
+ assertObject(args, "args");
30085
+ const selector = {
30086
+ ...typeof args.resourceId === "string" ? {
30087
+ resourceId: args.resourceId
30088
+ } : {},
30089
+ ...typeof args.text === "string" ? {
30090
+ text: args.text
30091
+ } : {},
30092
+ ...typeof args.contentDesc === "string" ? {
30093
+ contentDesc: args.contentDesc
30094
+ } : {}
30095
+ };
30096
+ if (!selector.resourceId && !selector.text && !selector.contentDesc)
30097
+ throw new Error("provide at least one of resourceId, text, or contentDesc");
30098
+ const timeoutSeconds = typeof args.timeoutSeconds === "number" && Number.isInteger(args.timeoutSeconds) ? Math.max(1, Math.min(120, args.timeoutSeconds)) : 15;
30099
+ const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
30100
+ const deadline = Date.now() + timeoutSeconds * 1000;
30101
+ let attempts = 0;
30102
+ while (Date.now() < deadline) {
30103
+ if (context.signal?.aborted)
30104
+ throw new Error("wait cancelled");
30105
+ attempts += 1;
30106
+ const node = findUiNode(parseUiNodes(await dumpHierarchy(context, serial)), selector);
30107
+ if (node)
30108
+ return {
30109
+ ok: true,
30110
+ summary: `element appeared after ${attempts} check(s)`,
30111
+ output: describeNode(node),
30112
+ metadata: {
30113
+ serial,
30114
+ node,
30115
+ attempts
30116
+ }
30117
+ };
30118
+ await new Promise((resolve9) => setTimeout(resolve9, 1000));
30119
+ }
30120
+ return {
30121
+ ok: false,
30122
+ summary: `element did not appear within ${timeoutSeconds}s`,
30123
+ output: "timed out waiting for the element",
30124
+ metadata: {
30125
+ serial,
30126
+ selector,
30127
+ attempts
30128
+ }
30129
+ };
30130
+ }
30131
+ };
30132
+ });
30133
+
30134
+ // src/agent-tools/android/frida.ts
30135
+ async function writeAsset(context, relPath, content) {
30136
+ const dir = relPath.includes("/") ? relPath.slice(0, relPath.lastIndexOf("/")) : ".";
30137
+ const b64 = Buffer.from(content, "utf8").toString("base64");
30138
+ const command = `mkdir -p ${shellQuote7(dir)} && printf %s ${shellQuote7(b64)} | base64 -d > ${shellQuote7(relPath)}`;
30139
+ const result = await backend(context).exec(command, 20000, context.signal);
30140
+ if (result.exitCode !== 0)
30141
+ throw new Error(`failed to write ${relPath}: ${compactError2(result.stderr || result.stdout)}`);
30142
+ }
30143
+ function fridaRunCommand(serial, mode, target, scriptPath, durationSeconds) {
30144
+ return [`${adbEnvPrefix()}python3`, RUNNER_PATH, "--serial", shellQuote7(serial), "--mode", shellQuote7(mode), "--target", shellQuote7(target), "--script", shellQuote7(scriptPath), "--duration", String(durationSeconds)].join(" ");
30145
+ }
30146
+ function parseFridaMessages(stdout) {
30147
+ const messages = [];
30148
+ let summary;
30149
+ for (const line of stdout.split(`
30150
+ `)) {
30151
+ const trimmed = line.trim();
30152
+ if (trimmed.startsWith("FRIDA_DONE ")) {
30153
+ try {
30154
+ summary = JSON.parse(trimmed.slice("FRIDA_DONE ".length));
30155
+ } catch {}
30156
+ } else if (trimmed.startsWith("FRIDA ")) {
30157
+ try {
30158
+ messages.push(JSON.parse(trimmed.slice("FRIDA ".length)));
30159
+ } catch {}
30160
+ }
30161
+ }
30162
+ return {
30163
+ messages,
30164
+ summary
30165
+ };
30166
+ }
30167
+ async function pipInstallFrida(context, version) {
30168
+ const spec = version ? `frida==${version} frida-tools` : "frida-tools";
30169
+ const install = await backend(context).exec(`pip install --upgrade ${spec} 2>&1`, 360000, context.signal, 2000000);
30170
+ const check = await backend(context).exec("frida --version 2>/dev/null", 15000, context.signal);
30171
+ return {
30172
+ output: install.stdout.trim().split(`
30173
+ `).slice(-15).join(`
30174
+ `),
30175
+ resolved: check.stdout.trim()
30176
+ };
30177
+ }
30178
+ var SERIAL_PROP4, DEVICE_SERVER_PATH = "/data/local/tmp/frida-server", ASSET_DIR = ".farai/android", RUNNER_PATH, ABI_MAP, FRIDA_RUNNER = `import sys, json, time, argparse
30179
+ try:
30180
+ import frida
30181
+ except Exception as exc:
30182
+ print("FRIDA_DONE " + json.dumps({"error": "frida python module missing: %s" % exc}))
30183
+ sys.exit(0)
30184
+
30185
+ def emit(obj):
30186
+ print("FRIDA " + json.dumps(obj), flush=True)
30187
+
30188
+ def main():
30189
+ ap = argparse.ArgumentParser()
30190
+ ap.add_argument("--serial", default="")
30191
+ ap.add_argument("--mode", default="attach")
30192
+ ap.add_argument("--target", required=True)
30193
+ ap.add_argument("--script", required=True)
30194
+ ap.add_argument("--duration", type=float, default=10.0)
30195
+ a = ap.parse_args()
30196
+ collected = []
30197
+ def on_message(message, data):
30198
+ if message.get("type") == "send":
30199
+ payload = message.get("payload")
30200
+ collected.append(payload)
30201
+ emit({"send": payload})
30202
+ elif message.get("type") == "error":
30203
+ desc = message.get("description")
30204
+ collected.append({"__error__": desc})
30205
+ emit({"error": desc})
30206
+ try:
30207
+ device = frida.get_device(a.serial, timeout=5) if a.serial else frida.get_usb_device(timeout=5)
30208
+ except Exception as exc:
30209
+ print("FRIDA_DONE " + json.dumps({"error": "device not reachable by frida: %s" % exc}))
30210
+ return
30211
+ try:
30212
+ with open(a.script) as handle:
30213
+ source = handle.read()
30214
+ except Exception as exc:
30215
+ print("FRIDA_DONE " + json.dumps({"error": "cannot read script: %s" % exc}))
30216
+ return
30217
+ spawned = False
30218
+ pid = None
30219
+ try:
30220
+ if a.mode == "spawn":
30221
+ pid = device.spawn([a.target])
30222
+ spawned = True
30223
+ session = device.attach(pid)
30224
+ else:
30225
+ session = device.attach(a.target)
30226
+ except Exception as exc:
30227
+ print("FRIDA_DONE " + json.dumps({"error": "attach/spawn failed: %s" % exc}))
30228
+ return
30229
+ try:
30230
+ script = session.create_script(source)
30231
+ script.on("message", on_message)
30232
+ script.load()
30233
+ if spawned and pid is not None:
30234
+ device.resume(pid)
30235
+ time.sleep(a.duration)
30236
+ except Exception as exc:
30237
+ collected.append({"__error__": str(exc)})
30238
+ finally:
30239
+ try:
30240
+ session.detach()
30241
+ except Exception:
30242
+ pass
30243
+ print("FRIDA_DONE " + json.dumps({"messages": collected, "spawned": spawned, "target": a.target}))
30244
+
30245
+ main()
30246
+ `, SSL_BYPASS_JS = `Java.perform(function () {
30247
+ function log(m) { send({ tag: "ssl-bypass", msg: m }); }
30248
+ try {
30249
+ var TMImpl = Java.use("com.android.org.conscrypt.TrustManagerImpl");
30250
+ TMImpl.checkTrustedRecursive.implementation = function () { log("conscrypt checkTrustedRecursive bypassed"); return Java.use("java.util.ArrayList").$new(); };
30251
+ } catch (e) {}
30252
+ try {
30253
+ var TrustManager = Java.registerClass({
30254
+ name: "com.farai.TrustAll",
30255
+ implements: [Java.use("javax.net.ssl.X509TrustManager")],
30256
+ methods: {
30257
+ checkClientTrusted: function () {},
30258
+ checkServerTrusted: function () {},
30259
+ getAcceptedIssuers: function () { return []; }
30260
+ }
30261
+ });
30262
+ var SSLContext = Java.use("javax.net.ssl.SSLContext");
30263
+ SSLContext.init.overload("[Ljavax.net.ssl.KeyManager;", "[Ljavax.net.ssl.TrustManager;", "java.security.SecureRandom").implementation = function (km, tm, sr) {
30264
+ log("SSLContext.init overridden with trust-all manager");
30265
+ this.init(km, [TrustManager.$new()], sr);
30266
+ };
30267
+ } catch (e) {}
30268
+ try {
30269
+ var OkHostnameVerifier = Java.use("okhttp3.internal.tls.OkHostnameVerifier");
30270
+ OkHostnameVerifier.verify.overload("java.lang.String", "javax.net.ssl.SSLSession").implementation = function () { log("okhttp OkHostnameVerifier bypassed"); return true; };
30271
+ } catch (e) {}
30272
+ try {
30273
+ var CertPinner = Java.use("okhttp3.CertificatePinner");
30274
+ CertPinner.check.overload("java.lang.String", "java.util.List").implementation = function () { log("okhttp CertificatePinner.check bypassed"); return; };
30275
+ } catch (e) {}
30276
+ log("ssl pinning bypass hooks installed");
30277
+ });
30278
+ `, ROOT_BYPASS_JS = `Java.perform(function () {
30279
+ function log(m) { send({ tag: "root-bypass", msg: m }); }
30280
+ try {
30281
+ var RootBeer = Java.use("com.scottyab.rootbeer.RootBeer");
30282
+ ["isRooted", "isRootedWithoutBusyBoxCheck", "detectRootManagementApps", "detectPotentiallyDangerousApps", "checkForBinary", "checkForSuBinary", "checkForDangerousProps", "detectTestKeys", "checkSuExists"].forEach(function (m) {
30283
+ try { RootBeer[m].implementation = function () { log("RootBeer." + m + " -> false"); return false; }; } catch (e) {}
30284
+ });
30285
+ } catch (e) {}
30286
+ try {
30287
+ var Runtime = Java.use("java.lang.Runtime");
30288
+ Runtime.exec.overload("java.lang.String").implementation = function (cmd) {
30289
+ if (cmd && (cmd.indexOf("su") !== -1 || cmd.indexOf("which") !== -1 || cmd.indexOf("busybox") !== -1)) { log("blocked Runtime.exec(" + cmd + ")"); throw Java.use("java.io.IOException").$new("blocked"); }
30290
+ return this.exec(cmd);
30291
+ };
30292
+ } catch (e) {}
30293
+ try {
30294
+ var File = Java.use("java.io.File");
30295
+ File.exists.implementation = function () {
30296
+ var path = this.getAbsolutePath();
30297
+ if (path && (path.indexOf("su") !== -1 || path.indexOf("magisk") !== -1 || path.indexOf("supersu") !== -1)) { log("hid file " + path); return false; }
30298
+ return this.exists();
30299
+ };
30300
+ } catch (e) {}
30301
+ log("root detection bypass hooks installed");
30302
+ });
30303
+ `, BYPASS_SCRIPTS, androidFridaInstallTool, androidFridaStatusTool, androidFridaSetupTool, androidFridaPsTool, androidFridaRunTool, androidFridaBypassTool;
30304
+ var init_frida = __esm(() => {
30305
+ init_backend();
30306
+ init_background_result();
30307
+ init_session_manager();
30308
+ init_renderers();
30309
+ init_shared4();
30310
+ SERIAL_PROP4 = {
30311
+ type: "string",
30312
+ description: "device serial from android_devices; omit when exactly one device is connected"
30313
+ };
30314
+ RUNNER_PATH = `${ASSET_DIR}/frida_runner.py`;
30315
+ ABI_MAP = {
30316
+ "arm64-v8a": "arm64",
30317
+ "armeabi-v7a": "arm",
30318
+ x86_64: "x86_64",
30319
+ x86: "x86"
30320
+ };
30321
+ BYPASS_SCRIPTS = {
30322
+ ssl: SSL_BYPASS_JS,
30323
+ root: ROOT_BYPASS_JS
30324
+ };
30325
+ androidFridaInstallTool = {
30326
+ name: "android_frida_install",
30327
+ description: "Install frida-tools in the container, optionally pinned to a version. Frida is intentionally not baked into the image because the python frida module must match the frida-server version, which varies per device and app. Install here first, then run android_frida_setup to provision a matching frida-server.",
30328
+ inputSchema: {
30329
+ type: "object",
30330
+ properties: {
30331
+ version: {
30332
+ type: "string",
30333
+ description: "exact frida version to pin, e.g. 16.5.9; omit to install the latest frida-tools"
30334
+ }
30335
+ },
30336
+ additionalProperties: false
30337
+ },
30338
+ mutates: true,
30339
+ timeoutMs: 360000,
30340
+ parallel: false,
30341
+ renderHuman: defaultHumanRenderer,
30342
+ renderModel: defaultModelRenderer,
30343
+ run: async (args, context) => {
30344
+ assertObject(args, "args");
30345
+ const version = typeof args.version === "string" && args.version.trim() ? args.version.trim() : "";
30346
+ if (version && !/^\d+(\.\d+){1,3}$/.test(version))
30347
+ throw new Error("version must look like 16.5.9");
30348
+ const spec = version ? `frida==${version} frida-tools` : "frida-tools";
30349
+ const command = `pip install --upgrade ${spec} 2>&1`;
30350
+ const result = await backend(context).exec(command, 360000, context.signal, 2000000);
30351
+ const installed = await backend(context).exec("frida --version 2>/dev/null", 15000, context.signal);
30352
+ const resolved = installed.stdout.trim();
30353
+ const ok = Boolean(resolved) && (!version || resolved === version);
30354
+ return {
30355
+ ok,
30356
+ summary: ok ? `frida-tools ${resolved} installed` : `frida install did not settle at the requested version${version ? ` (${version})` : ""}`,
30357
+ output: `${result.stdout}`.trim().split(`
30358
+ `).slice(-20).join(`
30359
+ `) || `exit ${result.exitCode}`,
30360
+ metadata: {
30361
+ requested: version || "latest",
30362
+ installedVersion: resolved || null
30363
+ }
30364
+ };
30365
+ }
30366
+ };
30367
+ androidFridaStatusTool = {
30368
+ name: "android_frida_status",
30369
+ description: "Check whether frida is ready: frida-tools in the container, and frida-server binary, process, and listening port on the device. Run this before other frida tools; if frida-tools is missing run android_frida_install, then android_frida_setup.",
30370
+ inputSchema: {
30371
+ type: "object",
30372
+ properties: {
30373
+ serial: SERIAL_PROP4
30374
+ },
30375
+ additionalProperties: false
30376
+ },
30377
+ mutates: false,
30378
+ timeoutMs: 40000,
30379
+ parallel: false,
30380
+ renderHuman: defaultHumanRenderer,
30381
+ renderModel: defaultModelRenderer,
30382
+ run: async (args, context) => {
30383
+ assertObject(args, "args");
30384
+ const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
30385
+ const version = await backend(context).exec("frida --version 2>/dev/null || true", 15000, context.signal);
30386
+ const fridaTools = version.stdout.trim();
30387
+ const binary = await runAdb(context, `${adbPrefix(serial)} shell ls ${DEVICE_SERVER_PATH} 2>/dev/null`, 15000);
30388
+ const proc = await runAdb(context, `${adbPrefix(serial)} shell 'ps -A 2>/dev/null | grep frida-server || ps | grep frida-server'`, 15000);
30389
+ const checks = {
30390
+ fridaToolsVersion: fridaTools || "missing",
30391
+ serverBinary: binary.stdout.includes("frida-server") ? "present" : "missing",
30392
+ serverProcess: /frida-server/.test(proc.stdout) ? "running" : "stopped"
30393
+ };
30394
+ const ready = Boolean(fridaTools) && checks.serverBinary === "present" && checks.serverProcess === "running";
30395
+ return {
30396
+ ok: true,
30397
+ summary: ready ? "frida is ready" : `frida is not ready; run ${fridaTools ? "android_frida_setup" : "android_frida_install then android_frida_setup"}`,
30398
+ output: Object.entries(checks).map(([key, value]) => `${key}: ${value}`).join(`
30399
+ `),
30400
+ metadata: {
30401
+ serial,
30402
+ ready,
30403
+ checks
30404
+ }
30405
+ };
30406
+ }
30407
+ };
30408
+ androidFridaSetupTool = {
30409
+ name: "android_frida_setup",
30410
+ description: "One-command frida provisioning: install frida-tools in the container (pinned to version when given), detect the device cpu abi, download the matching frida-server build, push it to /data/local/tmp/frida-server, and start it (needs root via su or adb root). The python frida module and frida-server versions are kept identical. Run android_frida_status afterwards to confirm.",
30411
+ inputSchema: {
30412
+ type: "object",
30413
+ properties: {
30414
+ version: {
30415
+ type: "string",
30416
+ description: "exact frida version to pin for both the python module and frida-server, e.g. 16.5.9; omit to use the latest installed frida-tools"
30417
+ },
30418
+ serial: SERIAL_PROP4
30419
+ },
30420
+ additionalProperties: false
30421
+ },
30422
+ mutates: true,
30423
+ timeoutMs: 500000,
30424
+ parallel: false,
30425
+ renderHuman: defaultHumanRenderer,
30426
+ renderModel: defaultModelRenderer,
30427
+ run: async (args, context) => {
30428
+ assertObject(args, "args");
30429
+ const requested = typeof args.version === "string" && args.version.trim() ? args.version.trim() : "";
30430
+ if (requested && !/^\d+(\.\d+){1,3}$/.test(requested))
30431
+ throw new Error("version must look like 16.5.9");
30432
+ const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
30433
+ const steps = [];
30434
+ let version = (await backend(context).exec("frida --version 2>/dev/null", 15000, context.signal)).stdout.trim();
30435
+ if (requested || !version) {
30436
+ const install = await pipInstallFrida(context, requested);
30437
+ version = install.resolved;
30438
+ steps.push(`pip install ${requested || "latest"}: ${version ? `frida-tools ${version}` : "failed"}`);
30439
+ } else {
30440
+ steps.push(`frida-tools already present: ${version}`);
30441
+ }
30442
+ if (!version)
30443
+ return {
30444
+ ok: false,
30445
+ summary: "frida-tools install failed",
30446
+ output: steps.join(`
30447
+ `),
30448
+ metadata: {
30449
+ serial
30450
+ }
30451
+ };
30452
+ const abiResult = await runAdb(context, `${adbPrefix(serial)} shell getprop ro.product.cpu.abi`, 15000);
30453
+ if (adbUnavailable(abiResult))
30454
+ throw new Error("adb is not available in the container");
30455
+ const abi = abiResult.stdout.trim().replace(/\r/g, "");
30456
+ const arch = ABI_MAP[abi];
30457
+ if (!arch)
30458
+ throw new Error(`unsupported device abi: ${abi || "unknown"} (supported: ${Object.keys(ABI_MAP).join(", ")})`);
30459
+ const url = `https://github.com/frida/frida/releases/download/${version}/frida-server-${version}-android-${arch}.xz`;
30460
+ const download = await backend(context).exec(`curl -fsSL -o /tmp/frida-server.xz ${shellQuote7(url)} && xz -d -f /tmp/frida-server.xz`, 180000, context.signal);
30461
+ steps.push(`download ${arch} ${version}: ${download.exitCode === 0 ? "ok" : `failed (${compactError2(download.stderr || download.stdout)})`}`);
30462
+ if (download.exitCode !== 0) {
30463
+ return {
30464
+ ok: false,
30465
+ summary: `could not download frida-server ${version} for ${arch}`,
30466
+ output: steps.join(`
30467
+ `),
30468
+ metadata: {
30469
+ serial,
30470
+ version,
30471
+ arch,
30472
+ url
30473
+ }
30474
+ };
30475
+ }
30476
+ const push = await runAdb(context, `${adbPrefix(serial)} push /tmp/frida-server ${DEVICE_SERVER_PATH} && ${adbPrefix(serial)} shell chmod 755 ${DEVICE_SERVER_PATH}`, 90000);
30477
+ steps.push(`push + chmod: ${push.exitCode === 0 ? "ok" : `failed (${compactError2(push.stderr || push.stdout)})`}`);
30478
+ const start = await runAdb(context, `${adbPrefix(serial)} shell 'su -c "${DEVICE_SERVER_PATH} -D" >/dev/null 2>&1 & echo started' || ${adbPrefix(serial)} shell '${DEVICE_SERVER_PATH} -D >/dev/null 2>&1 & echo started'`, 20000);
30479
+ steps.push(`start: ${/started/.test(start.stdout) ? "attempted (verify with android_frida_status)" : "could not start; device may need root"}`);
30480
+ return {
30481
+ ok: push.exitCode === 0,
30482
+ summary: `frida-server ${version} (${arch}) provisioned; verify with android_frida_status`,
30483
+ output: steps.join(`
30484
+ `),
30485
+ metadata: {
30486
+ serial,
30487
+ version,
30488
+ arch
30489
+ }
30490
+ };
30491
+ }
30492
+ };
30493
+ androidFridaPsTool = {
30494
+ name: "android_frida_ps",
30495
+ description: "List processes and applications visible to frida on the device. Use to find the exact process name or pid to attach to.",
30496
+ inputSchema: {
30497
+ type: "object",
30498
+ properties: {
30499
+ applicationsOnly: {
30500
+ type: "boolean",
30501
+ description: "list installed applications (frida-ps -Uai) instead of running processes when true"
30502
+ },
30503
+ serial: SERIAL_PROP4
30504
+ },
30505
+ additionalProperties: false
30506
+ },
30507
+ mutates: false,
30508
+ timeoutMs: 40000,
30509
+ parallel: false,
30510
+ renderHuman: defaultHumanRenderer,
30511
+ renderModel: defaultModelRenderer,
30512
+ run: async (args, context) => {
30513
+ assertObject(args, "args");
30514
+ const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
30515
+ const listFlag = args.applicationsOnly === true ? "-ai" : "-a";
30516
+ const deviceFlag = `-D ${shellQuote7(serial)}`;
30517
+ const result = await backend(context).exec(`${adbEnvPrefix()}frida-ps ${listFlag} ${deviceFlag} 2>&1`, 40000, context.signal);
30518
+ if (/not found|no such/i.test(result.stdout) && result.exitCode !== 0)
30519
+ throw new Error("frida-tools is not installed in the container");
30520
+ return {
30521
+ ok: result.exitCode === 0,
30522
+ summary: result.exitCode === 0 ? "listed frida targets" : "frida-ps failed (is frida-server running?)",
30523
+ output: result.stdout.trim() || "(no output)",
30524
+ metadata: {
30525
+ serial,
30526
+ listFlag
30527
+ }
30528
+ };
30529
+ }
30530
+ };
30531
+ androidFridaRunTool = {
30532
+ name: "android_frida_run",
30533
+ description: "Run a frida javascript script against an app and collect its send() messages. Write the script first with fs_write, then attach to a running process or spawn a package. Use background=true to keep hooks live while you interact with the app, then read output with session_poll.",
30534
+ inputSchema: {
30535
+ type: "object",
30536
+ required: ["scriptPath", "target"],
30537
+ properties: {
30538
+ scriptPath: {
30539
+ type: "string",
30540
+ description: "workspace-relative path to the frida javascript to load"
30541
+ },
30542
+ target: {
30543
+ type: "string",
30544
+ description: "package name to spawn, or process name/pid to attach to"
30545
+ },
30546
+ mode: {
30547
+ type: "string",
30548
+ enum: ["spawn", "attach"],
30549
+ description: "spawn launches the package fresh; attach hooks an already-running process (default attach)"
30550
+ },
30551
+ durationSeconds: {
30552
+ type: "integer",
30553
+ minimum: 1,
30554
+ maximum: 600,
30555
+ description: "how long to keep the session open collecting messages (default 15)"
30556
+ },
30557
+ background: {
30558
+ type: "boolean",
30559
+ description: "run as a persistent background session and return a job id; poll it with session_poll"
30560
+ },
30561
+ serial: SERIAL_PROP4
30562
+ },
30563
+ additionalProperties: false
30564
+ },
30565
+ mutates: true,
30566
+ timeoutMs: 620000,
30567
+ parallel: false,
30568
+ renderHuman: defaultHumanRenderer,
30569
+ renderModel: defaultModelRenderer,
30570
+ run: async (args, context) => {
30571
+ assertObject(args, "args");
30572
+ const scriptPath = asString(args.scriptPath, "scriptPath").trim();
30573
+ const target = asString(args.target, "target").trim();
30574
+ const mode = args.mode === "spawn" ? "spawn" : "attach";
30575
+ const durationSeconds = typeof args.durationSeconds === "number" && Number.isInteger(args.durationSeconds) ? Math.max(1, Math.min(600, args.durationSeconds)) : 15;
30576
+ const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
30577
+ await writeAsset(context, RUNNER_PATH, FRIDA_RUNNER);
30578
+ const command = fridaRunCommand(serial, mode, target, scriptPath, durationSeconds);
30579
+ if (args.background === true) {
30580
+ const started = await sessionManager.start(backend(context), "android_frida_run", command, clampYieldMs(args.background === true ? undefined : 1000), context.signal, {
30581
+ kind: "generic"
30582
+ });
30583
+ return backgroundToolResult("android_frida_run", started, "generic");
30584
+ }
30585
+ const result = await backend(context).exec(command, (durationSeconds + 30) * 1000, context.signal, 4000000);
30586
+ const {
30587
+ messages,
30588
+ summary
30589
+ } = parseFridaMessages(result.stdout);
30590
+ const error = summary && typeof summary.error === "string" ? summary.error : undefined;
30591
+ const output = error ? `frida error: ${error}` : `${messages.length} message(s):
30592
+ ${messages.map((m) => JSON.stringify(m)).join(`
30593
+ `)}`;
30594
+ return {
30595
+ ok: !error,
30596
+ summary: error ? `frida run failed: ${error}` : `collected ${messages.length} message(s) from ${target}`,
30597
+ output: output || "(no messages)",
30598
+ metadata: {
30599
+ serial,
30600
+ target,
30601
+ mode,
30602
+ messages: messages.slice(0, 500),
30603
+ ...summary ? {
30604
+ summary
30605
+ } : {}
30606
+ }
30607
+ };
30608
+ }
30609
+ };
30610
+ androidFridaBypassTool = {
30611
+ name: "android_frida_bypass",
30612
+ description: "Spawn an app with a bundled bypass script attached: ssl for certificate-pinning bypass (to see traffic through the proxy), root for root-detection bypass. Use background=true to keep the bypass active while you drive the app.",
30613
+ inputSchema: {
30614
+ type: "object",
30615
+ required: ["type", "package"],
30616
+ properties: {
30617
+ type: {
30618
+ type: "string",
30619
+ enum: ["ssl", "root"],
30620
+ description: "which bundled bypass to inject"
30621
+ },
30622
+ package: {
30623
+ type: "string",
30624
+ description: "package name to spawn with the bypass attached"
30625
+ },
30626
+ durationSeconds: {
30627
+ type: "integer",
30628
+ minimum: 1,
30629
+ maximum: 600,
30630
+ description: "how long to keep the bypass session open (default 30)"
30631
+ },
30632
+ background: {
30633
+ type: "boolean",
30634
+ description: "run as a persistent background session so hooks stay active; poll with session_poll"
30635
+ },
30636
+ serial: SERIAL_PROP4
30637
+ },
30638
+ additionalProperties: false
30639
+ },
30640
+ mutates: true,
30641
+ timeoutMs: 620000,
30642
+ parallel: false,
30643
+ renderHuman: defaultHumanRenderer,
30644
+ renderModel: defaultModelRenderer,
30645
+ run: async (args, context) => {
30646
+ assertObject(args, "args");
30647
+ const type = asString(args.type, "type");
30648
+ const script = BYPASS_SCRIPTS[type];
30649
+ if (!script)
30650
+ throw new Error(`unknown bypass type: ${type}; use ssl or root`);
30651
+ const pkg = asString(args.package, "package").trim();
30652
+ const durationSeconds = typeof args.durationSeconds === "number" && Number.isInteger(args.durationSeconds) ? Math.max(1, Math.min(600, args.durationSeconds)) : 30;
30653
+ const serial = await resolveDevice(context, typeof args.serial === "string" ? args.serial : undefined);
30654
+ const scriptPath = `${ASSET_DIR}/scripts/bypass_${type}.js`;
30655
+ await writeAsset(context, RUNNER_PATH, FRIDA_RUNNER);
30656
+ await writeAsset(context, scriptPath, script);
30657
+ const command = fridaRunCommand(serial, "spawn", pkg, scriptPath, durationSeconds);
30658
+ if (args.background === true) {
30659
+ const started = await sessionManager.start(backend(context), "android_frida_bypass", command, clampYieldMs(1000), context.signal, {
30660
+ kind: "generic"
30661
+ });
30662
+ return backgroundToolResult("android_frida_bypass", started, "generic");
30663
+ }
30664
+ const result = await backend(context).exec(command, (durationSeconds + 30) * 1000, context.signal, 4000000);
30665
+ const {
30666
+ messages,
30667
+ summary
30668
+ } = parseFridaMessages(result.stdout);
30669
+ const error = summary && typeof summary.error === "string" ? summary.error : undefined;
30670
+ return {
30671
+ ok: !error,
30672
+ summary: error ? `${type} bypass failed: ${error}` : `${type} bypass injected into ${pkg} (${messages.length} hook message(s))`,
30673
+ output: error ? `frida error: ${error}` : messages.map((m) => JSON.stringify(m)).join(`
30674
+ `) || "hooks installed (no messages emitted)",
30675
+ metadata: {
30676
+ serial,
30677
+ type,
30678
+ package: pkg,
30679
+ messages: messages.slice(0, 500)
30680
+ }
30681
+ };
30682
+ }
30683
+ };
30684
+ });
30685
+
30686
+ // src/agent-tools/android/index.ts
30687
+ var androidTools;
30688
+ var init_android = __esm(() => {
30689
+ init_device();
30690
+ init_app();
30691
+ init_static();
30692
+ init_ui();
30693
+ init_frida();
30694
+ androidTools = [androidConnectTool, androidDevicesTool, androidShellTool, androidPackagesTool, androidDeviceInfoTool, androidLogcatTool, androidApkPullTool, androidInstallTool, androidAppStartTool, androidAppStopTool, androidDeeplinkTool, androidPullFileTool, androidDecompileTool, androidManifestTool, androidPermissionsTool, androidExportedComponentsTool, androidScanSecretsTool, androidGrepApkTool, androidUiDumpTool, androidUiHierarchyTool, androidScreenshotTool, androidUiTapTool, androidUiTapElementTool, androidUiTypeTool, androidUiSwipeTool, androidUiKeyTool, androidUiWindowSizeTool, androidUiWaitForTool, androidFridaInstallTool, androidFridaStatusTool, androidFridaSetupTool, androidFridaPsTool, androidFridaRunTool, androidFridaBypassTool];
30695
+ });
30696
+
28684
30697
  // src/agent-tools/registry.ts
28685
30698
  function listToolsForSession(session) {
28686
30699
  const tools = [...baseTools, ...listMcpTools(session)];
@@ -28726,10 +30739,11 @@ var init_registry4 = __esm(() => {
28726
30739
  init_worktree();
28727
30740
  init_proxy();
28728
30741
  init_email();
30742
+ init_android();
28729
30743
  init_mcp_manager();
28730
30744
  init_process_output();
28731
30745
  init_mcp_manager();
28732
- baseTools = [...shellTools, ...reconTools, ...filesystemTools, ...gitTools, ...knowledgeTools, ...todoTools, ...reportTools, ...codegenTools, ...callbackTools, ...campaignTools, ...outputTools, ...lspTools, ...browserTools, ...kaliTools, ...agentTools, ...webTools, ...mediaTools, ...interactionTools, ...mcpResourceTools, ...worktreeTools, ...proxyTools, ...emailTools];
30746
+ baseTools = [...shellTools, ...reconTools, ...filesystemTools, ...gitTools, ...knowledgeTools, ...todoTools, ...reportTools, ...codegenTools, ...callbackTools, ...campaignTools, ...outputTools, ...lspTools, ...browserTools, ...kaliTools, ...agentTools, ...webTools, ...mediaTools, ...interactionTools, ...mcpResourceTools, ...worktreeTools, ...proxyTools, ...emailTools, ...androidTools];
28733
30747
  });
28734
30748
 
28735
30749
  // src/agent-core/mcp-server-management.ts
@@ -29928,12 +31942,25 @@ var init_tool_guidance = __esm(() => {
29928
31942
  });
29929
31943
 
29930
31944
  // src/agent-core/default-model.ts
29931
- var DEFAULT_MODEL_PROVIDER_ID = "opencode", DEFAULT_MODEL_BASE_URL = "https://opencode.ai/zen/v1", DEFAULT_MODEL_ID = "mimo-v2.5-free", DEFAULT_MODEL_PUBLIC_API_KEY = "public", DEFAULT_CONTEXT_WINDOW = 200000, DEFAULT_MAX_OUTPUT_TOKENS = 4096, DEFAULT_MAX_STEPS, DEFAULT_MAX_TURN_SECONDS;
31945
+ var DEFAULT_MODEL_PROVIDER_ID = "openrouter", DEFAULT_MODEL_BASE_URL = "https://openrouter.ai/api/v1", DEFAULT_MODEL_ID = "openrouter/free", DEFAULT_MODEL_PUBLIC_API_KEY = "", DEFAULT_CONTEXT_WINDOW = 200000, DEFAULT_MAX_OUTPUT_TOKENS = 4096, DEFAULT_MAX_STEPS, DEFAULT_MAX_TURN_SECONDS;
29932
31946
  var init_default_model = __esm(() => {
29933
31947
  DEFAULT_MAX_STEPS = Number.POSITIVE_INFINITY;
29934
31948
  DEFAULT_MAX_TURN_SECONDS = Number.POSITIVE_INFINITY;
29935
31949
  });
29936
31950
 
31951
+ // src/agent-core/default-source.ts
31952
+ function defaultSourceKey() {
31953
+ return Buffer.from(DEFAULT_SOURCE_KEY_B64, "base64").toString("utf8");
31954
+ }
31955
+ function isDefaultMode() {
31956
+ const config = loadGlobalConfig();
31957
+ return !config.baseUrl && !config.apiKeyEnv;
31958
+ }
31959
+ var DEFAULT_SOURCE_BASE_URL = "https://openrouter.ai/api/v1", DEFAULT_SOURCE_MODEL = "openrouter/free", DEFAULT_SOURCE_CONTEXT_WINDOW = 200000, DEFAULT_SOURCE_KEY_B64 = "c2stb3ItdjEtODM0NDYyZDI2YWMzODFkMDc5ZWI3N2Q1NTM2YWU0MTc2NWIyMGNhNWFiMzQ1YmEwNTJjNjU4ZTRhOWQ3ZGYzYQ==";
31960
+ var init_default_source = __esm(() => {
31961
+ init_global_config();
31962
+ });
31963
+
29937
31964
  // src/agent-core/model-registry.ts
29938
31965
  function resolveDefaultModel() {
29939
31966
  return resolveModel({});
@@ -29941,8 +31968,9 @@ function resolveDefaultModel() {
29941
31968
  function resolveModel(input = {}) {
29942
31969
  const config = loadGlobalConfig();
29943
31970
  const baseUrl = input.baseUrl ?? config.baseUrl ?? DEFAULT_MODEL_BASE_URL;
29944
- const model = input.model ?? config.model ?? DEFAULT_MODEL_ID;
29945
- const apiKey = input.apiKey ?? (config.apiKeyEnv ? process.env[config.apiKeyEnv] : undefined) ?? (baseUrl === DEFAULT_MODEL_BASE_URL ? DEFAULT_MODEL_PUBLIC_API_KEY : undefined);
31971
+ const isDefault = baseUrl === DEFAULT_MODEL_BASE_URL && !config.baseUrl;
31972
+ const model = input.model ?? config.model ?? (DEFAULT_MODEL_ID || undefined);
31973
+ const apiKey = input.apiKey ?? (config.apiKeyEnv ? process.env[config.apiKeyEnv] : undefined) ?? (isDefault ? defaultSourceKey() : DEFAULT_MODEL_PUBLIC_API_KEY || undefined);
29946
31974
  return {
29947
31975
  baseUrl,
29948
31976
  ...model ? {
@@ -30045,6 +32073,7 @@ var HEURISTIC_MODEL_ID = "heuristic", MODEL_DISCOVERY_TIMEOUT_MS = 4000, MODEL_D
30045
32073
  var init_model_registry = __esm(() => {
30046
32074
  init_global_config();
30047
32075
  init_default_model();
32076
+ init_default_source();
30048
32077
  init_http_response();
30049
32078
  MODEL_DISCOVERY_MAX_BYTES = 8 * 1024 * 1024;
30050
32079
  });
@@ -30394,6 +32423,15 @@ async function resolveModelSelection(workspace, selection) {
30394
32423
  }
30395
32424
  async function resolveDefaultCatalogModel(workspace) {
30396
32425
  const config = loadGlobalConfig();
32426
+ if (isDefaultMode()) {
32427
+ return {
32428
+ baseUrl: DEFAULT_SOURCE_BASE_URL,
32429
+ model: DEFAULT_SOURCE_MODEL,
32430
+ apiKey: defaultSourceKey(),
32431
+ contextWindow: DEFAULT_SOURCE_CONTEXT_WINDOW,
32432
+ name: "default"
32433
+ };
32434
+ }
30397
32435
  const catalog = await buildModelCatalog(workspace);
30398
32436
  const recent = readRecentModelSelections();
30399
32437
  for (const selection of recent) {
@@ -30403,9 +32441,6 @@ async function resolveDefaultCatalogModel(workspace) {
30403
32441
  }
30404
32442
  if (config.model)
30405
32443
  return resolveModelSelection(workspace, config.model);
30406
- const openCodeDefault = catalog.models.find((model) => model.providerID === DEFAULT_PROVIDER_ID && model.modelID === OPENCODE_DEFAULT_MODEL_ID);
30407
- if (openCodeDefault)
30408
- return withSavedModelLimits(modelChoiceToResolved(openCodeDefault), openCodeDefault.id, workspace);
30409
32444
  const first = sortModelChoices(catalog.models).find((model) => model.verified) ?? sortModelChoices(catalog.models)[0];
30410
32445
  if (first)
30411
32446
  return withSavedModelLimits(modelChoiceToResolved(first), first.id, workspace);
@@ -30449,7 +32484,7 @@ function normalizeModelsDevProviderHint(providerID, profile) {
30449
32484
  if (providerID !== DEFAULT_PROVIDER_ID)
30450
32485
  return providerID;
30451
32486
  if (!profile && resolveModel().baseUrl === DEFAULT_MODEL_BASE_URL)
30452
- return OPENCODE_PROVIDER_ID;
32487
+ return DEFAULT_SOURCE_PROVIDER_ID;
30453
32488
  return;
30454
32489
  }
30455
32490
  function readRecentModelSelections() {
@@ -30461,8 +32496,14 @@ function defaultModelSelection() {
30461
32496
  function displayModelSelection(workspace, selection) {
30462
32497
  if (selection) {
30463
32498
  const profile = loadModelProfiles(workspace).find((candidate) => candidate.name === selection);
30464
- return profile?.model ?? selection;
30465
- }
32499
+ if (profile)
32500
+ return profile.model ?? selection;
32501
+ if (isDefaultMode() && selection === DEFAULT_SOURCE_MODEL)
32502
+ return "default";
32503
+ return selection;
32504
+ }
32505
+ if (isDefaultMode())
32506
+ return "default";
30466
32507
  return defaultModelSelection() ?? "auto";
30467
32508
  }
30468
32509
  async function providerDefinitions(workspace, profiles) {
@@ -30490,39 +32531,21 @@ async function providerDefinitions(workspace, profiles) {
30490
32531
  for (const definition of profileDefinitions)
30491
32532
  if (definition)
30492
32533
  definitions2.push(definition);
30493
- const openCode = modelsDev?.[OPENCODE_PROVIDER_ID];
30494
- const configuredOpenCode = definitions2.some((provider) => provider.id === DEFAULT_PROVIDER_ID);
30495
- if (openCode && !configuredOpenCode) {
30496
- const hasApiKey = openCode.env.some((name) => Boolean(process.env[name]));
30497
- const freeModels = Object.values(openCode.models).filter((model) => hasApiKey || isFreeModel(model));
30498
- if (freeModels.length) {
30499
- definitions2.push({
30500
- id: DEFAULT_PROVIDER_ID,
30501
- name: openCode.name,
30502
- baseUrl: openCode.api,
30503
- ...openCode.env[0] ? {
30504
- apiKeyEnv: openCode.env[0]
30505
- } : {},
30506
- apiKey: openCode.env.map((name) => process.env[name]).find(Boolean) ?? OPENCODE_PUBLIC_API_KEY,
30507
- source: "models.dev",
30508
- catalogModels: freeModels.map((model) => ({
30509
- id: model.id,
30510
- ...model.name ? {
30511
- name: model.name
30512
- } : {},
30513
- free: isFreeModel(model),
30514
- ...model.limit?.context ? {
30515
- contextWindow: model.limit.context
30516
- } : {},
30517
- ...model.limit?.output ? {
30518
- maxOutputTokens: model.limit.output
30519
- } : {},
30520
- ...model.release_date ? {
30521
- releaseDate: model.release_date
30522
- } : {}
30523
- }))
30524
- });
30525
- }
32534
+ const configuredDefaultSource = definitions2.some((provider) => provider.id === DEFAULT_PROVIDER_ID);
32535
+ if (isDefaultMode() && !configuredDefaultSource) {
32536
+ definitions2.push({
32537
+ id: DEFAULT_PROVIDER_ID,
32538
+ name: "default",
32539
+ baseUrl: DEFAULT_SOURCE_BASE_URL,
32540
+ apiKey: defaultSourceKey(),
32541
+ source: "models.dev",
32542
+ catalogModels: [{
32543
+ id: DEFAULT_SOURCE_MODEL,
32544
+ name: "default",
32545
+ free: true,
32546
+ contextWindow: DEFAULT_SOURCE_CONTEXT_WINDOW
32547
+ }]
32548
+ });
30526
32549
  }
30527
32550
  return definitions2;
30528
32551
  }
@@ -30938,20 +32961,19 @@ function ensureConcrete(resolved) {
30938
32961
  function modelsDevCachePath() {
30939
32962
  return join18(globalDataDir(), "cache", "models-dev.json");
30940
32963
  }
30941
- var DEFAULT_PROVIDER_ID = "default", OPENCODE_PROVIDER_ID, OPENCODE_DEFAULT_MODEL_ID, OPENCODE_PUBLIC_API_KEY, MODELS_DEV_URL = "https://models.dev/api.json", MODELS_DEV_CACHE_TTL_MS, MODELS_DEV_STALE_TTL_MS, MODELS_DEV_FETCH_TIMEOUT_MS = 4000, MODELS_DEV_MAX_BYTES, RECENT_MODEL_LIMIT = 12, modelsDevRefreshes;
32964
+ var DEFAULT_PROVIDER_ID = "default", DEFAULT_SOURCE_PROVIDER_ID, MODELS_DEV_URL = "https://models.dev/api.json", MODELS_DEV_CACHE_TTL_MS, MODELS_DEV_STALE_TTL_MS, MODELS_DEV_FETCH_TIMEOUT_MS = 4000, MODELS_DEV_MAX_BYTES, RECENT_MODEL_LIMIT = 12, modelsDevRefreshes;
30942
32965
  var init_model_catalog = __esm(() => {
30943
32966
  init_default_model();
30944
32967
  init_global_config();
30945
32968
  init_config();
30946
32969
  init_model_registry();
30947
32970
  init_model_profiles();
32971
+ init_default_source();
30948
32972
  init_http_response();
30949
32973
  init_file_read();
30950
32974
  init_atomic_file();
30951
32975
  init_private_path();
30952
- OPENCODE_PROVIDER_ID = DEFAULT_MODEL_PROVIDER_ID;
30953
- OPENCODE_DEFAULT_MODEL_ID = DEFAULT_MODEL_ID;
30954
- OPENCODE_PUBLIC_API_KEY = DEFAULT_MODEL_PUBLIC_API_KEY;
32976
+ DEFAULT_SOURCE_PROVIDER_ID = DEFAULT_MODEL_PROVIDER_ID;
30955
32977
  MODELS_DEV_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
30956
32978
  MODELS_DEV_STALE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
30957
32979
  MODELS_DEV_MAX_BYTES = 16 * 1024 * 1024;
@@ -42958,6 +44980,17 @@ var init_runtime = __esm(() => {
42958
44980
 
42959
44981
  // src/branding.ts
42960
44982
  import figlet from "figlet";
44983
+ function printBannerOnce() {
44984
+ if (bannerPrinted)
44985
+ return;
44986
+ bannerPrinted = true;
44987
+ console.log(FARAI_BANNER);
44988
+ }
44989
+ function clearBannerIfShown() {
44990
+ if (!bannerPrinted || !process.stdout.isTTY)
44991
+ return;
44992
+ process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
44993
+ }
42961
44994
  function renderFaraiBanner() {
42962
44995
  try {
42963
44996
  return figlet.textSync("farai", {
@@ -42967,7 +45000,7 @@ function renderFaraiBanner() {
42967
45000
  return "farai";
42968
45001
  }
42969
45002
  }
42970
- var FARAI_BANNER, FARAI_BANNER_LINES;
45003
+ var FARAI_BANNER, FARAI_BANNER_LINES, bannerPrinted = false;
42971
45004
  var init_branding = __esm(() => {
42972
45005
  FARAI_BANNER = renderFaraiBanner();
42973
45006
  FARAI_BANNER_LINES = FARAI_BANNER.split(`
@@ -43812,6 +45845,7 @@ async function runStartupContentPreflight(workspace) {
43812
45845
  }
43813
45846
  async function promptForUpdate(version, knowledge, skills) {
43814
45847
  const contents = [knowledge ? "knowledge" : undefined, skills ? "skills" : undefined].filter(Boolean).join(" + ");
45848
+ printBannerOnce();
43815
45849
  console.log("");
43816
45850
  console.log(`farai content ${version} is available${contents ? ` (${contents})` : ""}`);
43817
45851
  const interfaceHandle = createInterface({
@@ -43866,6 +45900,7 @@ function errorMessage7(error) {
43866
45900
  return error instanceof Error ? error.message : String(error);
43867
45901
  }
43868
45902
  var init_preflight = __esm(() => {
45903
+ init_branding();
43869
45904
  init_config();
43870
45905
  init_updater();
43871
45906
  });
@@ -43903,8 +45938,7 @@ async function runStartupContainerPreflight(workspace) {
43903
45938
  return "continue";
43904
45939
  }
43905
45940
  async function promptForImagePull(exists) {
43906
- console.log("");
43907
- console.log(FARAI_BANNER);
45941
+ printBannerOnce();
43908
45942
  console.log("");
43909
45943
  console.log(exists ? "a newer kali container image is available" : "kali container image is not installed");
43910
45944
  const interfaceHandle = createInterface2({
@@ -56064,7 +58098,7 @@ function modelProviderOptions(choices, sessionModel) {
56064
58098
  return {
56065
58099
  id: `model-provider-${providerID}`,
56066
58100
  title: providerID,
56067
- description: [`${providerChoices.length} models`, freeCount ? `${freeCount} free` : undefined, readyCount ? `${readyCount} ready` : undefined, first?.baseUrl].filter(Boolean).join(" \xB7 "),
58101
+ description: [`${providerChoices.length} models`, freeCount ? `${freeCount} free` : undefined, readyCount ? `${readyCount} ready` : undefined, providerID === "default" ? undefined : first?.baseUrl].filter(Boolean).join(" \xB7 "),
56068
58102
  footer: current ? "current" : "",
56069
58103
  value: {
56070
58104
  kind: "model_provider",
@@ -58179,7 +60213,7 @@ function createComposerController(input) {
58179
60213
  composer.blur();
58180
60214
  try {
58181
60215
  renderer.suspend();
58182
- const proc = Bun.spawn(["sh", "-lc", `${editor} ${shellQuote7(file)}`], {
60216
+ const proc = Bun.spawn(["sh", "-lc", `${editor} ${shellQuote8(file)}`], {
58183
60217
  stdin: "inherit",
58184
60218
  stdout: "inherit",
58185
60219
  stderr: "inherit"
@@ -58244,7 +60278,7 @@ function createComposerController(input) {
58244
60278
  function normalizeComposerText(text2) {
58245
60279
  return text2.replace(/^\s*\n+/, "").replace(/\n+\s*$/, "").trim();
58246
60280
  }
58247
- function shellQuote7(value) {
60281
+ function shellQuote8(value) {
58248
60282
  return /^[A-Za-z0-9_./:@-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
58249
60283
  }
58250
60284
  function slashPromptText(rawText, completedTitle, slashName) {
@@ -65963,7 +67997,7 @@ function footerRightItems(backgroundActivities, subagents, browserContexts, queu
65963
67997
  items.push({
65964
67998
  id: "update",
65965
67999
  kind: "update",
65966
- text: `update ${updateNotice.latestVersion}`
68000
+ text: `update available ${updateNotice.latestVersion}`
65967
68001
  });
65968
68002
  }
65969
68003
  if (contextUsage && contextUsage.tokens >= 0) {
@@ -70452,7 +72486,7 @@ var init_app_shell = __esm(() => {
70452
72486
  function App() {
70453
72487
  return createComponent2(AppShell, {});
70454
72488
  }
70455
- var init_app = __esm(() => {
72489
+ var init_app2 = __esm(() => {
70456
72490
  init_solid2();
70457
72491
  init_app_shell();
70458
72492
  });
@@ -70815,7 +72849,7 @@ function formatResumeHint(sessionId, _title, options = {}) {
70815
72849
  const brand = options.styled ? `\x1B[2m${banner}\x1B[22m` : banner;
70816
72850
  const saved = options.styled ? "\x1B[2msession saved\x1B[22m" : "session saved";
70817
72851
  const usage = options.usage ? formatTokenUsage(options.usage) : undefined;
70818
- return ["", brand, "", saved, ` farai resume ${shellQuote8(sessionId)}`, "", usage, ""].filter((line) => line !== undefined).join(`
72852
+ return ["", brand, "", saved, ` farai resume ${shellQuote9(sessionId)}`, "", usage, ""].filter((line) => line !== undefined).join(`
70819
72853
  `);
70820
72854
  }
70821
72855
  function formatTokenUsage(usage) {
@@ -70840,7 +72874,7 @@ function exitBanner(width) {
70840
72874
  function terminalStylingEnabled() {
70841
72875
  return Boolean(process.stdout.isTTY && !process.env.NO_COLOR && process.env.TERM !== "dumb");
70842
72876
  }
70843
- function shellQuote8(value) {
72877
+ function shellQuote9(value) {
70844
72878
  return /^[A-Za-z0-9_./:@-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
70845
72879
  }
70846
72880
  async function launchOpenTui(workspace, sessionId) {
@@ -70875,7 +72909,7 @@ var init_agent_tui = __esm(() => {
70875
72909
  init_solid2();
70876
72910
  init_solid2();
70877
72911
  init_runtime();
70878
- init_app();
72912
+ init_app2();
70879
72913
  init_runtime2();
70880
72914
  init_store4();
70881
72915
  init_exit();
@@ -74189,6 +76223,7 @@ async function launchTui(workspace, sessionId) {
74189
76223
  process.exitCode = 130;
74190
76224
  return;
74191
76225
  }
76226
+ clearBannerIfShown();
74192
76227
  if (import.meta.path.endsWith(".ts")) {
74193
76228
  const sourceTuiPreload = "@opentui/solid/preload";
74194
76229
  await import(sourceTuiPreload);
@@ -74402,5 +76437,5 @@ Examples:
74402
76437
  `);
74403
76438
  }
74404
76439
 
74405
- //# debugId=32EC65247FDE45AE64756E2164756E21
76440
+ //# debugId=80A953E297D3B0EB64756E2164756E21
74406
76441
  //# sourceMappingURL=index.js.map