2 Commits

11 changed files with 616 additions and 165 deletions

View File

@@ -631,6 +631,13 @@ data: [DONE]
## Changelog ## Changelog
### v3.3.0 (2026-05-21)
- **File Manager** — browse device files (App Files, Downloads, Camera, Storage), open/preview any file, install APKs directly
- **SSH / Remote Access** — AI can SSH into external machines, SCP upload/download, curl URLs
- **Approval Gate** — all sensitive actions (SSH, SCP, curl, adb, pip, npm, etc.) require explicit user approval via in-app dialog
- **New Action Tags** — `[SSH_EXEC]`, `[SSH_UPLOAD]`, `[SSH_DOWNLOAD]`, `[REMOTE_EXEC]`, `[CURL_EXEC]`
- **Agentic Mode** — updated system prompt with full external access documentation
### v3.2.0 (2026-05-20) ### v3.2.0 (2026-05-20)
- **Full Internal Virtual Environment** — `BootstrapPlugin.setupVirtualEnv()` now provisions an app-contained Python venv under app storage, no external Termux app required - **Full Internal Virtual Environment** — `BootstrapPlugin.setupVirtualEnv()` now provisions an app-contained Python venv under app storage, no external Termux app required
- **In-App Module Installation** — `BootstrapPlugin.venvPipInstall()` installs Python modules directly into the internal venv - **In-App Module Installation** — `BootstrapPlugin.venvPipInstall()` installs Python modules directly into the internal venv

View File

@@ -21,8 +21,8 @@ android {
applicationId "ai.z.chat" applicationId "ai.z.chat"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 23 versionCode 25
versionName "3.2.0" versionName "3.3.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'

View File

@@ -593,78 +593,19 @@ public class BootstrapPlugin extends Plugin {
return; return;
} }
String pythonBin = prefix + "/bin/python3"; JSObject setup = setupVirtualEnvInternal(prefix, home, venvDir);
String pythonBinAlt = prefix + "/bin/python"; String pipBin = setup.getString("pip");
if (pipBin == null || !new File(pipBin).exists()) {
if (!new File(pythonBin).exists() && !new File(pythonBinAlt).exists()) { call.reject("Failed to prepare internal virtual environment");
Log.i(TAG, "Python not found, installing via pkg...");
String pkgBin = prefix + "/bin/pkg";
String aptBin = prefix + "/bin/apt";
String installBin = new File(pkgBin).exists() ? pkgBin : aptBin;
if (!new File(installBin).exists()) {
call.reject("Bootstrap not installed. Install dev tools first.");
return; return;
} }
ProcessBuilder pb = new ProcessBuilder("/system/bin/sh", "-c", String pipOutput = runBootstrapCommand(prefix, home,
"export PREFIX=\"" + prefix + "\" LD_LIBRARY_PATH=\"" + prefix + "/lib\" PATH=\"" + prefix + "/bin:/system/bin:$PATH\" HOME=\"" + home + "\" ANDROID_API_LEVEL=\"" + android.os.Build.VERSION.SDK_INT + "\" && " + "\"" + pipBin + "\" install --upgrade pip setuptools wheel && " +
"sh \"" + installBin + "\" install -y python clang rust make pkg-config libffi openssl 2>&1"); "\"" + pipBin + "\" install --prefer-binary hermes-agent");
pb.redirectErrorStream(true);
Process p = pb.start();
String installOutput = drainProcess(p);
p.waitFor(300, java.util.concurrent.TimeUnit.SECONDS);
Log.i(TAG, "Python install output: " + installOutput.substring(0, Math.min(500, installOutput.length())));
}
if (!new File(pythonBin).exists() && !new File(pythonBinAlt).exists()) { if (!new File(hermesLink).exists()) {
call.reject("Python installation failed. Try: pkg install python"); call.reject("hermes-agent installation failed: " + pipOutput.substring(0, Math.min(800, pipOutput.length())));
return;
}
String usePython = new File(pythonBin).exists() ? pythonBin : pythonBinAlt;
Log.i(TAG, "Creating Hermes Python venv...");
ProcessBuilder pb = new ProcessBuilder(usePython, "-m", "venv", venvDir);
pb.environment().put("ANDROID_API_LEVEL", String.valueOf(android.os.Build.VERSION.SDK_INT));
pb.environment().put("HOME", home);
pb.environment().put("LD_LIBRARY_PATH", prefix + "/lib");
pb.environment().put("PREFIX", prefix);
pb.redirectErrorStream(true);
Process p = pb.start();
String venvOutput = drainProcess(p);
p.waitFor(60, java.util.concurrent.TimeUnit.SECONDS);
Log.i(TAG, "venv output: " + venvOutput.substring(0, Math.min(300, venvOutput.length())));
String pipBin = venvDir + "/bin/pip";
if (!new File(pipBin).exists()) {
call.reject("Failed to create Python venv: " + venvOutput.substring(0, Math.min(200, venvOutput.length())));
return;
}
Log.i(TAG, "Upgrading pip...");
pb = new ProcessBuilder(pipBin, "install", "--upgrade", "pip", "setuptools", "wheel");
pb.environment().put("ANDROID_API_LEVEL", String.valueOf(android.os.Build.VERSION.SDK_INT));
pb.environment().put("HOME", home);
pb.environment().put("LD_LIBRARY_PATH", prefix + "/lib");
pb.redirectErrorStream(true);
p = pb.start();
drainProcess(p);
p.waitFor(120, java.util.concurrent.TimeUnit.SECONDS);
Log.i(TAG, "Installing hermes-agent...");
pb = new ProcessBuilder(pipBin, "install", "hermes-agent");
pb.environment().put("ANDROID_API_LEVEL", String.valueOf(android.os.Build.VERSION.SDK_INT));
pb.environment().put("HOME", home);
pb.environment().put("LD_LIBRARY_PATH", prefix + "/lib");
pb.environment().put("PREFIX", prefix);
pb.redirectErrorStream(true);
p = pb.start();
String pipOutput = drainProcess(p);
boolean pipOk = p.waitFor(600, java.util.concurrent.TimeUnit.SECONDS);
if (!new File(venvDir + "/bin/hermes").exists()) {
call.reject("hermes-agent installation failed: " + pipOutput.substring(0, Math.min(500, pipOutput.length())));
return; return;
} }
@@ -731,36 +672,7 @@ public class BootstrapPlugin extends Plugin {
String home = call.getString("home", homeDir + "/home"); String home = call.getString("home", homeDir + "/home");
String venvDir = call.getString("venv", filesDir + "/venv/default"); String venvDir = call.getString("venv", filesDir + "/venv/default");
new File(venvDir).getParentFile().mkdirs(); call.resolve(setupVirtualEnvInternal(prefix, home, venvDir));
String pkgBin = prefix + "/bin/pkg";
String aptBin = prefix + "/bin/apt";
String python3 = prefix + "/bin/python3";
if (!new File(pkgBin).exists() && !new File(aptBin).exists()) {
call.reject("Bootstrap tools missing. Install internal dev environment first.");
return;
}
if (!new File(python3).exists()) {
String installer = new File(pkgBin).exists() ? pkgBin : aptBin;
runBootstrapCommand(prefix, home,
"sh \"" + installer + "\" install -y python clang rust make pkg-config libffi openssl");
}
if (!new File(python3).exists()) {
call.reject("python3 unavailable after install");
return;
}
runBootstrapCommand(prefix, home,
"\"" + python3 + "\" -m venv \"" + venvDir + "\" && " +
"\"" + venvDir + "/bin/pip\" install --upgrade pip setuptools wheel");
call.resolve(new JSObject()
.put("ok", true)
.put("venv", venvDir)
.put("python", venvDir + "/bin/python")
.put("pip", venvDir + "/bin/pip"));
} catch (Exception e) { } catch (Exception e) {
try { call.reject("setupVirtualEnv failed: " + e.getMessage()); } catch (Exception ignored) {} try { call.reject("setupVirtualEnv failed: " + e.getMessage()); } catch (Exception ignored) {}
} }
@@ -788,7 +700,7 @@ public class BootstrapPlugin extends Plugin {
} }
String output = runBootstrapCommand(prefix, home, String output = runBootstrapCommand(prefix, home,
"\"" + pip + "\" install " + packages); "\"" + pip + "\" install --prefer-binary " + packages);
call.resolve(new JSObject().put("ok", true).put("output", output)); call.resolve(new JSObject().put("ok", true).put("output", output));
} catch (Exception e) { } catch (Exception e) {
try { call.reject("venvPipInstall failed: " + e.getMessage()); } catch (Exception ignored) {} try { call.reject("venvPipInstall failed: " + e.getMessage()); } catch (Exception ignored) {}
@@ -796,6 +708,46 @@ public class BootstrapPlugin extends Plugin {
}).start(); }).start();
} }
private JSObject setupVirtualEnvInternal(String prefix, String home, String venvDir) throws Exception {
new File(venvDir).getParentFile().mkdirs();
String pkgBin = prefix + "/bin/pkg";
String aptBin = prefix + "/bin/apt";
String python3 = prefix + "/bin/python3";
if (!new File(pkgBin).exists() && !new File(aptBin).exists()) {
throw new RuntimeException("Bootstrap tools missing. Install internal dev environment first.");
}
if (!new File(python3).exists()) {
String installer = new File(pkgBin).exists() ? pkgBin : aptBin;
runBootstrapCommand(prefix, home,
"sh \"" + installer + "\" install -y python clang rust make pkg-config libffi openssl");
}
if (!new File(python3).exists()) {
throw new RuntimeException("python3 unavailable after install");
}
String venvPython = venvDir + "/bin/python";
String venvPip = venvDir + "/bin/pip";
if (!new File(venvPip).exists()) {
runBootstrapCommand(prefix, home,
"\"" + python3 + "\" -m venv \"" + venvDir + "\"");
}
if (!new File(venvPip).exists()) {
throw new RuntimeException("Failed to create Python venv");
}
runBootstrapCommand(prefix, home,
"\"" + venvPip + "\" install --upgrade pip setuptools wheel");
return new JSObject()
.put("ok", true)
.put("venv", venvDir)
.put("python", venvPython)
.put("pip", venvPip);
}
private String runBootstrapCommand(String prefix, String home, String command) throws Exception { private String runBootstrapCommand(String prefix, String home, String command) throws Exception {
ProcessBuilder pb = new ProcessBuilder("/system/bin/sh", "-c", ProcessBuilder pb = new ProcessBuilder("/system/bin/sh", "-c",
"export PREFIX=\"" + prefix + "\" HOME=\"" + home + "\" " + "export PREFIX=\"" + prefix + "\" HOME=\"" + home + "\" " +

View File

@@ -0,0 +1,160 @@
package ai.z.chat;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;
import android.webkit.MimeTypeMap;
import androidx.core.content.FileProvider;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@CapacitorPlugin(name = "FileManager")
public class FileManagerPlugin extends Plugin {
@PluginMethod
public void openFile(PluginCall call) {
String path = call.getString("path", "");
String mimeType = call.getString("mimeType", "");
if (path.isEmpty()) {
call.reject("No path provided");
return;
}
File file = new File(path);
if (!file.exists()) {
call.reject("File not found: " + path);
return;
}
try {
Context context = getContext();
Uri uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
String resolvedMime = mimeType.isEmpty() ? guessMimeType(file.getName()) : mimeType;
if (resolvedMime == null || resolvedMime.isEmpty()) resolvedMime = "*/*";
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(uri, resolvedMime);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
call.resolve(new JSObject().put("opened", true).put("path", file.getAbsolutePath()).put("mimeType", resolvedMime));
} catch (Exception e) {
call.reject("Open failed: " + e.getMessage());
}
}
@PluginMethod
public void listFiles(PluginCall call) {
String rootPath = call.getString("path", "");
if (rootPath.isEmpty()) {
call.reject("No path provided");
return;
}
File root = new File(rootPath);
if (!root.exists()) {
call.reject("Path not found: " + rootPath);
return;
}
JSObject result = new JSObject();
result.put("path", root.getAbsolutePath());
result.put("name", root.getName());
result.put("directory", root.isDirectory());
result.put("size", root.isFile() ? root.length() : 0);
result.put("mimeType", root.isFile() ? guessMimeType(root.getName()) : "inode/directory");
if (root.isDirectory()) {
File[] children = root.listFiles();
List<JSObject> items = new ArrayList<>();
if (children != null) {
Arrays.sort(children, (a, b) -> a.getName().compareToIgnoreCase(b.getName()));
for (File child : children) {
JSObject item = new JSObject();
item.put("name", child.getName());
item.put("path", child.getAbsolutePath());
item.put("directory", child.isDirectory());
item.put("size", child.isFile() ? child.length() : 0);
item.put("mimeType", child.isFile() ? guessMimeType(child.getName()) : "inode/directory");
items.add(item);
}
}
result.put("items", items);
}
call.resolve(result);
}
@PluginMethod
public void openContainingFolder(PluginCall call) {
String path = call.getString("path", "");
if (path.isEmpty()) {
call.reject("No path provided");
return;
}
File file = new File(path);
File target = file.isDirectory() ? file : file.getParentFile();
if (target == null || !target.exists()) {
call.reject("Containing folder not found");
return;
}
call.resolve(new JSObject().put("opened", true).put("path", target.getAbsolutePath()));
}
@PluginMethod
public void getRoots(PluginCall call) {
JSObject result = new JSObject();
List<JSObject> roots = new ArrayList<>();
roots.add(rootItem("App Files", getContext().getFilesDir()));
File ext = getContext().getExternalFilesDir(null);
if (ext != null) roots.add(rootItem("External Files", ext));
File downloads = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
if (downloads != null) roots.add(rootItem("Downloads", downloads));
File dcim = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
if (dcim != null) roots.add(rootItem("Camera", dcim));
File sdcard = Environment.getExternalStorageDirectory();
if (sdcard != null) roots.add(rootItem("Storage Root", sdcard));
result.put("roots", roots);
call.resolve(result);
}
private JSObject rootItem(String label, File file) {
JSObject item = new JSObject();
item.put("name", label);
item.put("path", file.getAbsolutePath());
item.put("directory", true);
item.put("size", 0);
item.put("mimeType", "inode/directory");
return item;
}
private String guessMimeType(String name) {
String lower = name.toLowerCase();
if (lower.endsWith(".apk")) return "application/vnd.android.package-archive";
if (lower.endsWith(".apks")) return "application/vnd.android.package-archive";
if (lower.endsWith(".zip")) return "application/zip";
if (lower.endsWith(".json")) return "application/json";
if (lower.endsWith(".html") || lower.endsWith(".htm")) return "text/html";
if (lower.endsWith(".md") || lower.endsWith(".txt") || lower.endsWith(".log")) return "text/plain";
String ext = MimeTypeMap.getFileExtensionFromUrl(name);
if (ext == null || ext.isEmpty()) return "*/*";
String guess = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.toLowerCase());
return guess != null ? guess : "*/*";
}
}

View File

@@ -15,6 +15,9 @@ import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin; import com.getcapacitor.annotation.CapacitorPlugin;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
@CapacitorPlugin(name = "Installer") @CapacitorPlugin(name = "Installer")
public class InstallerPlugin extends Plugin { public class InstallerPlugin extends Plugin {
@@ -58,6 +61,45 @@ public class InstallerPlugin extends Plugin {
} }
} }
@PluginMethod
public void exportApk(PluginCall call) {
String path = call.getString("path", "");
String name = call.getString("name", "app.apk");
if (path.isEmpty()) {
call.reject("No path provided");
return;
}
File apkFile = new File(path);
if (!apkFile.exists()) {
call.reject("APK file not found: " + path);
return;
}
try {
Context context = getContext();
File outRoot = context.getExternalFilesDir(android.os.Environment.DIRECTORY_DOWNLOADS);
if (outRoot == null) {
outRoot = context.getFilesDir();
}
File outDir = new File(outRoot, "ZAI-Chat");
if (!outDir.exists()) outDir.mkdirs();
File outFile = new File(outDir, name);
try (FileInputStream fis = new FileInputStream(apkFile);
FileOutputStream fos = new FileOutputStream(outFile)) {
byte[] buf = new byte[8192];
int r;
while ((r = fis.read(buf)) > 0) fos.write(buf, 0, r);
}
call.resolve(new JSObject()
.put("exported", true)
.put("path", outFile.getAbsolutePath())
.put("name", name));
} catch (Exception e) {
call.reject("Export failed: " + e.getMessage());
}
}
@PluginMethod @PluginMethod
public void getDeviceInfo(PluginCall call) { public void getDeviceInfo(PluginCall call) {
JSObject info = new JSObject(); JSObject info = new JSObject();

View File

@@ -9,6 +9,7 @@ public class MainActivity extends BridgeActivity {
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
registerPlugin(ShellPlugin.class); registerPlugin(ShellPlugin.class);
registerPlugin(InstallerPlugin.class); registerPlugin(InstallerPlugin.class);
registerPlugin(FileManagerPlugin.class);
registerPlugin(WakePlugin.class); registerPlugin(WakePlugin.class);
registerPlugin(BootstrapPlugin.class); registerPlugin(BootstrapPlugin.class);
registerPlugin(AutoGLMPlugin.class); registerPlugin(AutoGLMPlugin.class);

View File

@@ -1,6 +1,6 @@
{ {
"name": "zai-chat", "name": "zai-chat",
"version": "3.2.0", "version": "3.3.0",
"description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan", "description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@@ -98,8 +98,11 @@
<div id="file-tree-panel" class="file-tree-panel"> <div id="file-tree-panel" class="file-tree-panel">
<div class="file-tree-header"> <div class="file-tree-header">
<h3>Project Files</h3> <h3>Project Files</h3>
<div style="display:flex;gap:8px;align-items:center;">
<button id="device-files-btn" class="icon-btn" aria-label="Open device files" title="Device files">&#128193;</button>
<button id="file-tree-close" class="icon-btn" aria-label="Close file tree">&times;</button> <button id="file-tree-close" class="icon-btn" aria-label="Close file tree">&times;</button>
</div> </div>
</div>
<div id="file-tree-body" class="file-tree-body"> <div id="file-tree-body" class="file-tree-body">
<div class="ftree-empty">No files yet.<br>AI-generated files appear here.</div> <div class="ftree-empty">No files yet.<br>AI-generated files appear here.</div>
</div> </div>
@@ -338,13 +341,23 @@
</div> </div>
<div class="settings-section"> <div class="settings-section">
<h3>About</h3> <h3>About</h3>
<p class="about-text">Z.AI Chat v3.2.0</p> <p class="about-text">Z.AI Chat v3.3.0</p>
<p class="about-text">Built with Z.AI SDK &amp; GLM-5.1</p> <p class="about-text">Built with Z.AI SDK &amp; GLM-5.1</p>
<p class="about-text">Compatible with Android 15/16</p> <p class="about-text">Compatible with Android 15/16</p>
</div> </div>
<div class="settings-section"> <div class="settings-section">
<h3>Changelog</h3> <h3>Changelog</h3>
<ul class="changelog-list"> <ul class="changelog-list">
<li>
<span class="changelog-version">v3.3.0</span>
<span class="changelog-date">2026-05-21</span>
<ul>
<li><strong>File Manager</strong> — browse device files, open/preview any file, install APKs directly</li>
<li><strong>SSH / Remote Access</strong> — AI can SSH into external machines, SCP files, curl URLs (user approves each)</li>
<li><strong>Approval Gate</strong> — all sensitive actions (SSH, SCP, curl, adb, pip, etc.) require your explicit approval</li>
<li><strong>New Action Tags</strong> — [SSH_EXEC], [SSH_UPLOAD], [SSH_DOWNLOAD], [REMOTE_EXEC], [CURL_EXEC]</li>
</ul>
</li>
<li> <li>
<span class="changelog-version">v3.2.0</span> <span class="changelog-version">v3.2.0</span>
<span class="changelog-date">2026-05-20</span> <span class="changelog-date">2026-05-20</span>
@@ -612,6 +625,21 @@
</div> </div>
</div> </div>
</div> </div>
<div id="approval-modal" class="file-viewer" style="display:none;z-index:3000;">
<div class="file-viewer-header">
<div class="file-viewer-title">
<span id="approval-title">Approval required</span>
</div>
<div class="file-viewer-actions">
<button id="approval-deny" class="fv-btn">Deny</button>
<button id="approval-allow" class="fv-btn fv-btn-save">Approve</button>
</div>
</div>
<div class="file-viewer-body" style="padding:16px;">
<pre id="approval-body" style="white-space:pre-wrap;margin:0;"></pre>
</div>
</div>
</div> </div>
</div> </div>
<script src="js/marked.min.js"></script> <script src="js/marked.min.js"></script>

View File

@@ -8,7 +8,7 @@
chat: 'You are a helpful, knowledgeable AI assistant. Be concise and accurate.', chat: 'You are a helpful, knowledgeable AI assistant. Be concise and accurate.',
coding: 'You are an expert coding assistant with internal tool access. Use action tags when execution is needed: [CREATE_FILE ...][/CREATE_FILE], [RUN_COMMAND]...[/RUN_COMMAND], [BUILD_APK project], [INSTALL_APK path], [VENV_SETUP], [VENV_PIP_INSTALL package]. Keep responses concise and complete.', coding: 'You are an expert coding assistant with internal tool access. Use action tags when execution is needed: [CREATE_FILE ...][/CREATE_FILE], [RUN_COMMAND]...[/RUN_COMMAND], [BUILD_APK project], [INSTALL_APK path], [VENV_SETUP], [VENV_PIP_INSTALL package]. Keep responses concise and complete.',
brainstorm: 'You are a creative brainstorming partner. Generate diverse ideas, explore unconventional angles, build on concepts, and help evaluate trade-offs. Think freely and expansively. Present ideas in organized lists or tables when appropriate.', brainstorm: 'You are a creative brainstorming partner. Generate diverse ideas, explore unconventional angles, build on concepts, and help evaluate trade-offs. Think freely and expansively. Present ideas in organized lists or tables when appropriate.',
agentic: 'You are an autonomous agent with full control of this app sandbox: internal Linux bootstrap, terminal, virtual env, build tools, and device UI via AutoGLM.\n\n## File Operations:\n[CREATE_FILE path/to/file.ext]\ncontents\n[/CREATE_FILE]\n\n## Shell:\n[RUN_COMMAND]\ncommand\n[/RUN_COMMAND]\n\n## Build:\n[BUILD_APK project_name]\n[INSTALL_APK /path/to/file.apk]\n\n## In-App Virtual Env:\n[VENV_SETUP]\n[VENV_PIP_INSTALL package_name]\n\n## AutoGLM Device Control:\n[DEVICE_TAP x y]\n[DEVICE_LONG_PRESS x y]\n[DEVICE_SWIPE startX startY endX endY]\n[DEVICE_TYPE text]\n[DEVICE_PRESS_BACK]\n[DEVICE_PRESS_HOME]\n[DEVICE_PRESS_RECENTS]\n[DEVICE_SCREENSHOT]\n[DEVICE_UI_TREE]\n[DEVICE_CLICK_TEXT button text]\n[DEVICE_CLICK_ID com.example:id/viewId]\n[DEVICE_LAUNCH com.example.app]\n[DEVICE_CURRENT_APP]\n\n## Hermes Agent:\n[HERMES_INSTALL]\n[HERMES_EXEC command]\n\n## Rules:\n1. Prefer internal sandbox tools and virtual env first\n2. Use [CREATE_FILE], then [BUILD_APK], then [INSTALL_APK]\n3. Use [DEVICE_*] for device control; start with [DEVICE_UI_TREE]\n4. Generate complete files, never stubs\n5. For Java: package ai.z.app, target SDK 36\n6. Output [TASK_COMPLETE] only when all work is done' agentic: 'You are an autonomous agent with full control of this app: internal Linux, terminal, virtual env, build tools, device UI, and EXTERNAL network access.\n\n## File Operations:\n[CREATE_FILE path/to/file.ext]\ncontents\n[/CREATE_FILE]\n\n## Shell (any command — runs inside app sandbox):\n[RUN_COMMAND]\ncommand\n[/RUN_COMMAND]\n\n## External SSH — run commands on remote machines:\n[SSH_EXEC user@host command]\n[SSH_UPLOAD local_path user@host:remote_path]\n[SSH_DOWNLOAD user@host:remote_path local_path]\n\n## External network tools:\n[REMOTE_EXEC host command]\n[CURL_EXEC url]\n\n## Build:\n[BUILD_APK project_name]\n[INSTALL_APK /path/to/file.apk]\n\n## In-App Virtual Env:\n[VENV_SETUP]\n[VENV_PIP_INSTALL package_name]\n\n## AutoGLM Device Control:\n[DEVICE_TAP x y]\n[DEVICE_LONG_PRESS x y]\n[DEVICE_SWIPE startX startY endX endY]\n[DEVICE_TYPE text]\n[DEVICE_PRESS_BACK]\n[DEVICE_PRESS_HOME]\n[DEVICE_PRESS_RECENTS]\n[DEVICE_SCREENSHOT]\n[DEVICE_UI_TREE]\n[DEVICE_CLICK_TEXT button text]\n[DEVICE_CLICK_ID com.example:id/viewId]\n[DEVICE_LAUNCH com.example.app]\n[DEVICE_CURRENT_APP]\n\n## Hermes Agent:\n[HERMES_INSTALL]\n[HERMES_EXEC command]\n\n## Rules:\n1. Prefer internal sandbox tools first\n2. Use SSH/REMOTE actions for external machines — user must approve each one\n3. Use [CREATE_FILE], then [BUILD_APK], then [INSTALL_APK]\n4. Use [DEVICE_*] for device control; start with [DEVICE_UI_TREE]\n5. Generate complete files, never stubs\n6. For Java: package ai.z.app, target SDK 36\n7. For SSH: assume openssh-client is available, use key-based auth when possible\n8. Output [TASK_COMPLETE] only when all work is done'
}; };
var BUILD_SCRIPT = [ var BUILD_SCRIPT = [
@@ -118,6 +118,8 @@
terminalOpen: false, terminalOpen: false,
keepAwake: false, keepAwake: false,
autoDeploy: true, autoDeploy: true,
autoInstallBuiltApk: true,
lastBuiltApkPath: '',
maxRetries: 10, maxRetries: 10,
autoContinue: true, autoContinue: true,
maxAutoContinue: 5 maxAutoContinue: 5
@@ -140,6 +142,7 @@
state.terminalOpen = localStorage.getItem(STORAGE_KEY + 'terminalOpen') === 'true'; state.terminalOpen = localStorage.getItem(STORAGE_KEY + 'terminalOpen') === 'true';
state.keepAwake = localStorage.getItem(STORAGE_KEY + 'keepAwake') === 'true'; state.keepAwake = localStorage.getItem(STORAGE_KEY + 'keepAwake') === 'true';
state.autoDeploy = localStorage.getItem(STORAGE_KEY + 'autoDeploy') !== 'false'; state.autoDeploy = localStorage.getItem(STORAGE_KEY + 'autoDeploy') !== 'false';
state.autoInstallBuiltApk = localStorage.getItem(STORAGE_KEY + 'autoInstallBuiltApk') !== 'false';
state.maxRetries = parseInt(localStorage.getItem(STORAGE_KEY + 'maxRetries')) || 10; state.maxRetries = parseInt(localStorage.getItem(STORAGE_KEY + 'maxRetries')) || 10;
state.autoContinue = localStorage.getItem(STORAGE_KEY + 'autoContinue') !== 'false'; state.autoContinue = localStorage.getItem(STORAGE_KEY + 'autoContinue') !== 'false';
state.maxAutoContinue = parseInt(localStorage.getItem(STORAGE_KEY + 'maxAutoContinue')) || 5; state.maxAutoContinue = parseInt(localStorage.getItem(STORAGE_KEY + 'maxAutoContinue')) || 5;
@@ -163,6 +166,7 @@
localStorage.setItem(STORAGE_KEY + 'terminalOpen', state.terminalOpen.toString()); localStorage.setItem(STORAGE_KEY + 'terminalOpen', state.terminalOpen.toString());
localStorage.setItem(STORAGE_KEY + 'keepAwake', state.keepAwake.toString()); localStorage.setItem(STORAGE_KEY + 'keepAwake', state.keepAwake.toString());
localStorage.setItem(STORAGE_KEY + 'autoDeploy', state.autoDeploy.toString()); localStorage.setItem(STORAGE_KEY + 'autoDeploy', state.autoDeploy.toString());
localStorage.setItem(STORAGE_KEY + 'autoInstallBuiltApk', state.autoInstallBuiltApk.toString());
localStorage.setItem(STORAGE_KEY + 'maxRetries', state.maxRetries.toString()); localStorage.setItem(STORAGE_KEY + 'maxRetries', state.maxRetries.toString());
localStorage.setItem(STORAGE_KEY + 'autoContinue', state.autoContinue.toString()); localStorage.setItem(STORAGE_KEY + 'autoContinue', state.autoContinue.toString());
localStorage.setItem(STORAGE_KEY + 'maxAutoContinue', state.maxAutoContinue.toString()); localStorage.setItem(STORAGE_KEY + 'maxAutoContinue', state.maxAutoContinue.toString());
@@ -1180,16 +1184,19 @@
autoglmEnabled: false, autoglmEnabled: false,
commandQueue: [] commandQueue: []
}; };
var approvalState = null;
function initShellPlugins() { function initShellPlugins() {
try { try {
Shell = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Shell; Shell = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Shell;
Installer = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Installer; Installer = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Installer;
FileManager = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.FileManager;
Wake = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Wake; Wake = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Wake;
Bootstrap = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Bootstrap; Bootstrap = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Bootstrap;
} catch(e) {} } catch(e) {}
if (!Shell) console.warn('Shell plugin not available'); if (!Shell) console.warn('Shell plugin not available');
if (!Installer) console.warn('Installer plugin not available'); if (!Installer) console.warn('Installer plugin not available');
if (!FileManager) console.warn('FileManager plugin not available');
if (!Wake) console.warn('Wake plugin not available'); if (!Wake) console.warn('Wake plugin not available');
if (!Bootstrap) console.warn('Bootstrap plugin not available'); if (!Bootstrap) console.warn('Bootstrap plugin not available');
} }
@@ -1213,6 +1220,11 @@
} }
} }
function needsApprovalForCommand(command) {
var cmd = (command || '').trim();
return /(^|\s)(ssh|scp|sftp|curl|wget|adb|am\s+start|pm\s+install|install\s+|chmod|chown|rm\s+-rf|mv\s+|cp\s+|python|python3|pip|npm|git\s+push|git\s+pull)/i.test(cmd);
}
async function shellWriteFile(path, content) { async function shellWriteFile(path, content) {
if (!Shell) return false; if (!Shell) return false;
try { try {
@@ -1244,6 +1256,60 @@
} }
} }
async function openFile(path, mimeType) {
if (!FileManager) { termPrint('[FileManager plugin not available]', 'err'); return; }
try {
await FileManager.openFile({ path: path, mimeType: mimeType || '' });
termPrint('[Open file triggered: ' + path + ']', 'success');
} catch(e) {
termPrint('[Open failed: ' + e.message + ']', 'err');
}
}
function requestApproval(title, body, onApprove) {
approvalState = { onApprove: onApprove };
var modal = $('#approval-modal');
var titleEl = $('#approval-title');
var bodyEl = $('#approval-body');
if (titleEl) titleEl.textContent = title || 'Approval required';
if (bodyEl) bodyEl.textContent = body || '';
if (modal) modal.style.display = 'flex';
}
function closeApproval() {
var modal = $('#approval-modal');
if (modal) modal.style.display = 'none';
approvalState = null;
}
async function askApproval(title, body, action) {
return new Promise(function(resolve) {
requestApproval(title, body, async function() {
try {
var result = await action();
resolve(result);
} catch(e) {
resolve(false);
} finally {
closeApproval();
}
});
});
}
async function listFiles(path) {
if (!FileManager) return null;
try { return await FileManager.listFiles({ path: path }); } catch(e) { return null; }
}
function isApkPath(path) {
return !!path && path.toLowerCase().endsWith('.apk');
}
function isOpenablePath(path) {
return !!path && /\.(apk|apks|zip|json|txt|log|md|html?|xml|js|java|kt|png|jpg|jpeg|webp)$/i.test(path);
}
async function getDeviceInfo() { async function getDeviceInfo() {
if (!Installer) return {}; if (!Installer) return {};
try { return await Installer.getDeviceInfo(); } catch(e) { return {}; } try { return await Installer.getDeviceInfo(); } catch(e) { return {}; }
@@ -1294,6 +1360,18 @@
if (!command.trim()) return; if (!command.trim()) return;
if (termState.isRunning) return; if (termState.isRunning) return;
if (needsApprovalForCommand(command)) {
await askApproval('Run command?', command, function() {
return shellExec(command, termState.cwd, false).then(function(result) {
termPrint('$ ' + command, 'cmd');
if (result.output) termPrint(result.output.replace(/\n$/, ''), '');
if (result.exitCode !== 0 && result.exitCode !== undefined) termPrint('[exit code: ' + result.exitCode + ']', 'err');
return true;
});
});
return;
}
termState.history.push(command); termState.history.push(command);
termState.historyIndex = termState.history.length; termState.historyIndex = termState.history.length;
termPrint('$ ' + command, 'cmd'); termPrint('$ ' + command, 'cmd');
@@ -1439,6 +1517,11 @@
var deviceClickIdRegex = /\[DEVICE_CLICK_ID\s+([^\]]+)\]/gi; var deviceClickIdRegex = /\[DEVICE_CLICK_ID\s+([^\]]+)\]/gi;
var deviceLaunchRegex = /\[DEVICE_LAUNCH\s+([^\]]+)\]/gi; var deviceLaunchRegex = /\[DEVICE_LAUNCH\s+([^\]]+)\]/gi;
var deviceCurrentAppRegex = /\[DEVICE_CURRENT_APP\]/gi; var deviceCurrentAppRegex = /\[DEVICE_CURRENT_APP\]/gi;
var sshExecRegex = /\[SSH_EXEC\s+([^\s]+)\s+([^\]]+)\]/gi;
var sshUploadRegex = /\[SSH_UPLOAD\s+([^\s]+)\s+([^\]]+)\]/gi;
var sshDownloadRegex = /\[SSH_DOWNLOAD\s+([^\s]+)\s+([^\]]+)\]/gi;
var remoteExecRegex = /\[REMOTE_EXEC\s+([^\s]+)\s+([^\]]+)\]/gi;
var curlExecRegex = /\[CURL_EXEC\s+([^\]]+)\]/gi;
var hermesInstallRegex = /\[HERMES_INSTALL\]/gi; var hermesInstallRegex = /\[HERMES_INSTALL\]/gi;
var hermesExecRegex = /\[HERMES_EXEC\s+([^\]]+)\]/gi; var hermesExecRegex = /\[HERMES_EXEC\s+([^\]]+)\]/gi;
var venvSetupRegex = /\[VENV_SETUP\]/gi; var venvSetupRegex = /\[VENV_SETUP\]/gi;
@@ -1509,6 +1592,21 @@
while ((match = venvPipInstallRegex.exec(content)) !== null) { while ((match = venvPipInstallRegex.exec(content)) !== null) {
actions.push({ type: 'venv_pip_install', packages: match[1].trim() }); actions.push({ type: 'venv_pip_install', packages: match[1].trim() });
} }
while ((match = sshExecRegex.exec(content)) !== null) {
actions.push({ type: 'ssh_exec', host: match[1].trim(), command: match[2].trim() });
}
while ((match = sshUploadRegex.exec(content)) !== null) {
actions.push({ type: 'ssh_upload', localPath: match[1].trim(), remotePath: match[2].trim() });
}
while ((match = sshDownloadRegex.exec(content)) !== null) {
actions.push({ type: 'ssh_download', remotePath: match[1].trim(), localPath: match[2].trim() });
}
while ((match = remoteExecRegex.exec(content)) !== null) {
actions.push({ type: 'remote_exec', host: match[1].trim(), command: match[2].trim() });
}
while ((match = curlExecRegex.exec(content)) !== null) {
actions.push({ type: 'curl_exec', url: match[1].trim() });
}
while ((match = codeBlockFileRegex.exec(content)) !== null) { while ((match = codeBlockFileRegex.exec(content)) !== null) {
var lang = match[1]; var lang = match[1];
var code = match[2]; var code = match[2];
@@ -1682,7 +1780,10 @@
content.indexOf('[INSTALL_APK') >= 0 || content.indexOf('[INSTALL_APK') >= 0 ||
content.indexOf('[DEVICE_') >= 0 || content.indexOf('[DEVICE_') >= 0 ||
content.indexOf('[HERMES_') >= 0 || content.indexOf('[HERMES_') >= 0 ||
content.indexOf('[VENV_') >= 0; content.indexOf('[VENV_') >= 0 ||
content.indexOf('[SSH_') >= 0 ||
content.indexOf('[REMOTE_EXEC') >= 0 ||
content.indexOf('[CURL_EXEC') >= 0;
var hasCodeBlock = content.indexOf('```') >= 0; var hasCodeBlock = content.indexOf('```') >= 0;
if (!hasCodeBlock && !hasAction && content.length < 300) return true; if (!hasCodeBlock && !hasAction && content.length < 300) return true;
if ((content.match(/```/g) || []).length % 2 !== 0) return false; if ((content.match(/```/g) || []).length % 2 !== 0) return false;
@@ -1842,6 +1943,9 @@
html += 'data-conv="' + convId + '" data-path="' + escapeAttr(file.path) + '">'; html += 'data-conv="' + convId + '" data-path="' + escapeAttr(file.path) + '">';
html += '<span class="ftree-ext">' + escapeHtml(file.language || '?') + '</span>'; html += '<span class="ftree-ext">' + escapeHtml(file.language || '?') + '</span>';
html += '<span class="ftree-fname">' + escapeHtml(file.name) + '</span>'; html += '<span class="ftree-fname">' + escapeHtml(file.name) + '</span>';
if (isApkPath(file.path)) {
html += '<span class="ftree-badge">APK</span>';
}
html += '</div>'; html += '</div>';
} }
return html; return html;
@@ -1863,6 +1967,62 @@
if (badge) badge.textContent = conv.files.length + ' file' + (conv.files.length !== 1 ? 's' : ''); if (badge) badge.textContent = conv.files.length + ' file' + (conv.files.length !== 1 ? 's' : '');
} }
async function renderDeviceFiles(rootPath) {
var body = $('#file-tree-body');
if (!body) return;
var result = await listFiles(rootPath);
if (!result) {
body.innerHTML = '<div class="ftree-empty">File manager unavailable.</div>';
return;
}
var items = result.items || [];
if (items.length === 0) {
body.innerHTML = '<div class="ftree-empty">Folder is empty.</div>';
return;
}
var html = '<div class="device-files-root">';
for (var i = 0; i < items.length; i++) {
var item = items[i];
html += '<div class="ftree-node ftree-file device-file-row" data-path="' + escapeAttr(item.path) + '">';
html += '<span class="ftree-ext">' + escapeHtml(item.directory ? 'dir' : (item.mimeType || '?')) + '</span>';
html += '<span class="ftree-fname">' + escapeHtml(item.name) + '</span>';
html += item.directory ? '<span class="ftree-badge">DIR</span>' : (isApkPath(item.path) ? '<span class="ftree-badge">APK</span>' : '');
html += '</div>';
}
html += '</div>';
body.innerHTML = html;
}
async function renderFileManagerHome() {
var body = $('#file-tree-body');
if (!body) return;
if (!FileManager) {
body.innerHTML = '<div class="ftree-empty">File manager unavailable.</div>';
return;
}
try {
var result = await FileManager.getRoots();
var roots = result.roots || [];
var html = '<div class="device-files-root">';
for (var i = 0; i < roots.length; i++) {
var item = roots[i];
html += '<div class="ftree-node ftree-file device-file-row" data-path="' + escapeAttr(item.path) + '">';
html += '<span class="ftree-ext">root</span>';
html += '<span class="ftree-fname">' + escapeHtml(item.name) + '</span>';
html += '<span class="ftree-badge">OPEN</span>';
html += '</div>';
}
html += '</div>';
body.innerHTML = html;
var title = document.querySelector('#file-tree-panel h3');
if (title) title.textContent = 'File Manager';
var count = $('#file-tree-count');
if (count) count.textContent = roots.length + ' roots';
} catch(e) {
body.innerHTML = '<div class="ftree-empty">File manager unavailable.</div>';
}
}
function toggleFileTree() { function toggleFileTree() {
var panel = $('#file-tree-panel'); var panel = $('#file-tree-panel');
var overlay = $('#file-tree-overlay'); var overlay = $('#file-tree-overlay');
@@ -1878,6 +2038,28 @@
} }
} }
function toggleDeviceFileManager() {
var panel = $('#file-tree-panel');
var overlay = $('#file-tree-overlay');
if (!panel) return;
if (panel.classList.contains('open')) {
panel.classList.remove('open');
if (overlay) overlay.classList.remove('active');
return;
}
panel.classList.add('open');
if (overlay) overlay.classList.add('active');
renderFileManagerHome();
}
function showProjectFiles() {
renderFileTree();
var title = document.querySelector('#file-tree-panel h3');
if (title) title.textContent = 'Project Files';
var count = $('#file-tree-count');
if (count) count.textContent = (getConversation() && getConversation().files ? getConversation().files.length : 0) + ' files';
}
function closeFileTree() { function closeFileTree() {
var panel = $('#file-tree-panel'); var panel = $('#file-tree-panel');
var overlay = $('#file-tree-overlay'); var overlay = $('#file-tree-overlay');
@@ -1920,6 +2102,19 @@
viewer.dataset.path = path; viewer.dataset.path = path;
} }
async function openFileFromTree(path) {
if (isApkPath(path)) {
await askApproval('Install APK?', path, function() { return installApk(path); });
return;
}
var fileManagerTitle = document.querySelector('#file-tree-panel h3');
if (fileManagerTitle && fileManagerTitle.textContent === 'File Manager') {
await renderDeviceFiles(path);
return;
}
await openFile(path, '');
}
function closeFileViewer() { function closeFileViewer() {
var viewer = $('#file-viewer'); var viewer = $('#file-viewer');
if (viewer) viewer.style.display = 'none'; if (viewer) viewer.style.display = 'none';
@@ -2325,9 +2520,10 @@
var hasDevice = actions.some(function(a) { return a.type && a.type.indexOf('device_') === 0; }); var hasDevice = actions.some(function(a) { return a.type && a.type.indexOf('device_') === 0; });
var hasHermes = actions.some(function(a) { return a.type && a.type.indexOf('hermes_') === 0; }); var hasHermes = actions.some(function(a) { return a.type && a.type.indexOf('hermes_') === 0; });
var hasVenv = actions.some(function(a) { return a.type && a.type.indexOf('venv_') === 0; }); var hasVenv = actions.some(function(a) { return a.type && a.type.indexOf('venv_') === 0; });
var hasSsh = actions.some(function(a) { return a.type && a.type.indexOf('ssh_') === 0; });
var hasRemote = actions.some(function(a) { return a.type === 'remote_exec' || a.type === 'curl_exec'; });
if (!hasFiles && !hasBuild && !hasInstall && !hasCommands && !hasDevice && !hasHermes && !hasVenv) return; if (!hasFiles && !hasBuild && !hasInstall && !hasCommands && !hasDevice && !hasHermes && !hasVenv && !hasSsh && !hasRemote) return;
_agenticRetryCount = 0; _agenticRetryCount = 0;
var resultLog = []; var resultLog = [];
@@ -2346,17 +2542,82 @@
if (hasCommands) { if (hasCommands) {
for (var c = 0; c < actions.length; c++) { for (var c = 0; c < actions.length; c++) {
if (actions[c].type === 'run_command') { if (actions[c].type === 'run_command') {
showStatusToast('Running: ' + actions[c].command.substring(0, 40), 'info'); var cmd = actions[c].command;
var cmdResult = await shellExec(actions[c].command, termState.cwd, false); showStatusToast('Running: ' + cmd.substring(0, 40), 'info');
resultLog.push('CMD: ' + actions[c].command.substring(0, 60) + '\nexit: ' + (cmdResult.exitCode !== undefined ? cmdResult.exitCode : '?') + '\n' + (cmdResult.output || '').substring(0, 500)); var cmdApproved = true;
if (needsApprovalForCommand(cmd)) {
cmdApproved = await askApproval('Run command?', cmd, function() { return true; });
}
if (!cmdApproved) {
termPrint('[DENIED] ' + cmd, 'warning');
resultLog.push('DENIED: ' + cmd.substring(0, 60));
continue;
}
var cmdResult = await shellExec(cmd, termState.cwd, false);
resultLog.push('CMD: ' + cmd.substring(0, 60) + '\nexit: ' + (cmdResult.exitCode !== undefined ? cmdResult.exitCode : '?') + '\n' + (cmdResult.output || '').substring(0, 500));
if (cmdResult.output) { if (cmdResult.output) {
termPrint('$ ' + actions[c].command, 'cmd'); termPrint('$ ' + cmd, 'cmd');
termPrint(cmdResult.output.replace(/\n$/, ''), cmdResult.exitCode === 0 ? '' : 'err'); termPrint(cmdResult.output.replace(/\n$/, ''), cmdResult.exitCode === 0 ? '' : 'err');
} }
} }
} }
} }
if (hasSsh) {
for (var s = 0; s < actions.length; s++) {
var sAct = actions[s];
if (sAct.type.indexOf('ssh_') !== 0) continue;
var sshCmd = '';
var sshLabel = '';
if (sAct.type === 'ssh_exec') {
sshCmd = 'ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ' + sAct.host + ' ' + sAct.command;
sshLabel = 'SSH: ' + sAct.host + ' → ' + sAct.command.substring(0, 60);
} else if (sAct.type === 'ssh_upload') {
sshCmd = 'scp -o StrictHostKeyChecking=no -o ConnectTimeout=10 ' + sAct.localPath + ' ' + sAct.remotePath;
sshLabel = 'SCP upload: ' + sAct.localPath + ' → ' + sAct.remotePath;
} else if (sAct.type === 'ssh_download') {
sshCmd = 'scp -o StrictHostKeyChecking=no -o ConnectTimeout=10 ' + sAct.remotePath + ' ' + sAct.localPath;
sshLabel = 'SCP download: ' + sAct.remotePath + ' → ' + sAct.localPath;
}
var sshOk = await askApproval(sshLabel, sshCmd, function() { return true; });
if (!sshOk) {
termPrint('[DENIED] ' + sshLabel, 'warning');
resultLog.push('DENIED: ' + sshLabel);
continue;
}
termPrint('$ ' + sshCmd, 'cmd');
var sshResult = await shellExec(sshCmd, termState.cwd, false);
resultLog.push('SSH: ' + sshLabel + '\nexit: ' + (sshResult.exitCode !== undefined ? sshResult.exitCode : '?') + '\n' + (sshResult.output || '').substring(0, 500));
termPrint((sshResult.output || '').replace(/\n$/, ''), sshResult.exitCode === 0 ? '' : 'err');
if (sshResult.exitCode !== 0 && sshResult.exitCode !== undefined) {
termPrint('[SSH exit: ' + sshResult.exitCode + ']', 'err');
}
}
}
if (hasRemote) {
for (var r = 0; r < actions.length; r++) {
var rAct = actions[r];
if (rAct.type === 'remote_exec') {
var reCmd = 'ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ' + rAct.host + ' ' + rAct.command;
var reOk = await askApproval('Remote exec: ' + rAct.host, reCmd, function() { return true; });
if (!reOk) { termPrint('[DENIED] ' + rAct.host, 'warning'); continue; }
termPrint('$ ' + reCmd, 'cmd');
var reResult = await shellExec(reCmd, termState.cwd, false);
resultLog.push('REMOTE: ' + rAct.host + '\n' + (reResult.output || '').substring(0, 500));
termPrint((reResult.output || '').replace(/\n$/, ''), reResult.exitCode === 0 ? '' : 'err');
} else if (rAct.type === 'curl_exec') {
var curlCmd = 'curl -sL -o - ' + rAct.url;
var curlOk = await askApproval('Fetch URL?', rAct.url, function() { return true; });
if (!curlOk) { termPrint('[DENIED] curl ' + rAct.url, 'warning'); continue; }
termPrint('$ ' + curlCmd, 'cmd');
var curlResult = await shellExec(curlCmd, termState.cwd, false);
resultLog.push('CURL: ' + rAct.url + '\n' + (curlResult.output || '').substring(0, 500));
termPrint((curlResult.output || '').replace(/\n$/, ''), '');
}
}
}
if (hasDevice) { if (hasDevice) {
for (var d = 0; d < actions.length; d++) { for (var d = 0; d < actions.length; d++) {
var act = actions[d]; var act = actions[d];
@@ -2578,8 +2839,28 @@
if (verifyResult.output && verifyResult.output.indexOf('No such file') < 0) { if (verifyResult.output && verifyResult.output.indexOf('No such file') < 0) {
var sizeMatch = verifyResult.output.match(/(\d+)\s+/); var sizeMatch = verifyResult.output.match(/(\d+)\s+/);
var sizeInfo = sizeMatch ? ' (' + Math.round(parseInt(sizeMatch[1]) / 1024) + ' KB)' : ''; var sizeInfo = sizeMatch ? ' (' + Math.round(parseInt(sizeMatch[1]) / 1024) + ' KB)' : '';
termPrint('\n[VERIFIED] APK built successfully: ' + apkPath + sizeInfo, 'success'); var builtName = projectDir.split('/').pop() + '.apk';
return '[BUILD OK] ' + apkPath; var Installer = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Installer;
var exportedPath = apkPath;
try {
if (Installer) {
var exportResult = await Installer.exportApk({ path: apkPath, name: builtName });
exportedPath = exportResult.path || apkPath;
state.lastBuiltApkPath = exportedPath;
}
} catch(e) {
termPrint('[!] Export to real folder failed: ' + e.message, 'warning');
}
termPrint('\n[VERIFIED] APK built successfully: ' + exportedPath + sizeInfo, 'success');
if (state.autoInstallBuiltApk && Installer) {
try {
await Installer.installApk({ path: exportedPath });
termPrint('[OK] Installer opened for built APK', 'success');
} catch(e) {
termPrint('[!] Auto-install failed: ' + e.message, 'warning');
}
}
return '[BUILD OK] ' + exportedPath;
} else { } else {
termPrint('\n[VERIFY FAILED] Build claimed success but APK not found at ' + apkPath, 'err'); termPrint('\n[VERIFY FAILED] Build claimed success but APK not found at ' + apkPath, 'err');
return '[BUILD FAILED] APK file not found after build. Output:\n' + output.substring(0, 1000); return '[BUILD FAILED] APK file not found after build. Output:\n' + output.substring(0, 1000);
@@ -2678,55 +2959,14 @@
showScreen('terminal'); showScreen('terminal');
termPrint('\n--- Building APK ---', 'info'); termPrint('\n--- Building APK ---', 'info');
var projectDir = termState.projectsDir || (termState.homeDir + '/projects'); var buildResult = await autoBuildApk(actions);
termPrint(buildResult, buildResult.indexOf('[BUILD FAILED]') >= 0 ? 'err' : 'success');
for (var i = 0; i < actions.length; i++) { if (state.autoInstallBuiltApk && buildResult.indexOf('[BUILD OK]') >= 0) {
var action = actions[i]; var Installer = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Installer;
if (action.type === 'create_file') { if (Installer && state.lastBuiltApkPath) {
var path = action.path; await Installer.installApk({ path: state.lastBuiltApkPath });
if (!path.startsWith('/')) path = projectDir + '/' + path;
var dir = path.substring(0, path.lastIndexOf('/'));
await shellMkdirs(dir);
await shellWriteFile(path, action.content);
termPrint(' [+] ' + path, 'success');
} }
} }
termPrint('\nBuilding with aapt2 + d8...', 'info');
var buildCmd = 'cd ' + projectDir + ' && ' +
'if [ -d "app/src/main" ]; then ' +
' AAPT2=$(which aapt2 2>/dev/null || echo "") && ' +
' D8=$(which d8 2>/dev/null || echo "") && ' +
' ECJ=$(which ecj 2>/dev/null || echo "") && ' +
' if [ -z "$AAPT2" ]; then echo "[!] aapt2 not found. Run Setup Dev Tools first."; exit 1; fi && ' +
' echo "[*] Compiling resources..." && ' +
' $AAPT2 compile --dir app/src/main/res -o build/compiled_resources.zip 2>&1 && ' +
' echo "[*] Linking..." && ' +
' $AAPT2 link -o build/app.unsigned.apk ' +
' -I tools/android.jar ' +
' --manifest app/src/main/AndroidManifest.xml ' +
' -R build/compiled_resources.zip ' +
' --java build/gen 2>&1 && ' +
' echo "[*] Compiling Java..." && ' +
' find app/src/main/java -name "*.java" > build/sources.txt 2>/dev/null && ' +
' $ECJ -source 11 -target 11 -classpath tools/android.jar -d build/classes @build/sources.txt 2>&1 && ' +
' echo "[*] Converting to DEX..." && ' +
' $D8 --output build/ build/classes/**/*.class 2>&1 && ' +
' echo "[*] Packaging..." && ' +
' cd build && cp app.unsigned.apk app.unaligned.apk && ' +
' mkdir -p app.unaligned.apk.tmp && cd app.unaligned.apk.tmp && ' +
' unzip -o ../app.unaligned.apk && ' +
' cp ../classes.dex . && ' +
' zip -r ../app.unaligned.apk . && cd .. && rm -rf app.unaligned.apk.tmp && ' +
' echo "[*] Signing..." && ' +
' java -jar tools/uber-apk-signer.jar -a app.unaligned.apk --overwrite 2>&1 || ' +
' cp app.unaligned.apk app-signed.apk && ' +
' echo "[OK] APK built: ' + projectDir + '/build/app-signed.apk" && ' +
' echo "Size: $(du -h app-signed.apk | cut -f1)" ; ' +
'else echo "[!] No app/src/main found. Deploy files first."; fi';
await termExec(buildCmd);
} }
// ---- Dev Tools Setup ---- // ---- Dev Tools Setup ----
@@ -2952,7 +3192,7 @@
termState.projectsDir = env.PROJECTS; termState.projectsDir = env.PROJECTS;
termState.cwd = env.CWD || env.HOME; termState.cwd = env.CWD || env.HOME;
updateCwdDisplay(); updateCwdDisplay();
termPrint('Z.AI Terminal v1.3.0', 'info'); termPrint('Z.AI Terminal v3.3.0', 'info');
termPrint('Home: ' + termState.homeDir, 'info'); termPrint('Home: ' + termState.homeDir, 'info');
termPrint('Type "help" for commands, "setup" for dev tools\n', 'info'); termPrint('Type "help" for commands, "setup" for dev tools\n', 'info');
}).catch(function() {}); }).catch(function() {});
@@ -3238,13 +3478,30 @@
saveState(); saveState();
}); });
$('#file-tree-btn').addEventListener('click', toggleFileTree); $('#file-tree-btn').addEventListener('click', toggleDeviceFileManager);
$('#device-files-btn').addEventListener('click', function() {
var panel = $('#file-tree-panel');
if (panel && panel.classList.contains('open')) {
showProjectFiles();
} else {
toggleDeviceFileManager();
}
});
$('#file-tree-close').addEventListener('click', closeFileTree); $('#file-tree-close').addEventListener('click', closeFileTree);
$('#file-tree-overlay').addEventListener('click', closeFileTree); $('#file-tree-overlay').addEventListener('click', closeFileTree);
$('#file-tree-body').addEventListener('click', function(e) { $('#file-tree-body').addEventListener('click', function(e) {
var node = e.target.closest('.ftree-file'); var node = e.target.closest('.ftree-file');
if (node) { if (node) {
if (node.dataset.conv) {
openFileViewer(node.dataset.conv, node.dataset.path); openFileViewer(node.dataset.conv, node.dataset.path);
} else {
var title = document.querySelector('#file-tree-panel h3');
if (title && title.textContent === 'File Manager') {
renderDeviceFiles(node.dataset.path);
} else {
openFileFromTree(node.dataset.path);
}
}
} else { } else {
node = e.target.closest('.ftree-dir'); node = e.target.closest('.ftree-dir');
if (node) { if (node) {
@@ -3258,6 +3515,10 @@
$('#file-viewer-close').addEventListener('click', closeFileViewer); $('#file-viewer-close').addEventListener('click', closeFileViewer);
$('#file-viewer-edit').addEventListener('click', toggleFileEdit); $('#file-viewer-edit').addEventListener('click', toggleFileEdit);
$('#file-viewer-save').addEventListener('click', saveFileEdit); $('#file-viewer-save').addEventListener('click', saveFileEdit);
$('#approval-allow').addEventListener('click', function() {
if (approvalState && approvalState.onApprove) approvalState.onApprove();
});
$('#approval-deny').addEventListener('click', closeApproval);
$('#theme-toggle-header').addEventListener('click', toggleTheme); $('#theme-toggle-header').addEventListener('click', toggleTheme);