@@ -4,7 +4,12 @@ import android.content.Context
44import android.util.Log
55import kotlinx.coroutines.Dispatchers
66import kotlinx.coroutines.withContext
7+ import org.json.JSONArray
8+ import org.json.JSONObject
79import java.io.File
10+ import java.io.OutputStreamWriter
11+ import java.net.HttpURLConnection
12+ import java.net.URL
813
914/* *
1015 * Manages the NullClaw binary lifecycle.
@@ -112,64 +117,68 @@ class ClawRunner(private val context: Context) {
112117 context.getSharedPreferences(PREFS_NAME , Context .MODE_PRIVATE )
113118 .getString(PREF_API_KEY , null )
114119
120+ // System prompt for voice responses
121+ private val SYSTEM_PROMPT = " You are a voice assistant on a Samsung smartwatch. " +
122+ " Rules: respond in 1-3 short sentences maximum. No markdown, no lists, no bullet points. " +
123+ " Plain spoken language only. Be direct and precise. Never say 'Certainly!' or 'Great question!'"
124+
115125 /* *
116- * Run a NullClaw agent query .
117- * Fix #7: stdout and stderr read on separate threads to prevent pipe buffer deadlock .
126+ * Call Anthropic API directly via Android's HTTP stack .
127+ * Samsung Wear OS blocks network from subprocesses so we can't use NullClaw's curl .
118128 */
119129 suspend fun query (prompt : String ): Result <String > = withContext(Dispatchers .IO ) {
120130 val apiKey = getApiKey()
121131 ? : return @withContext Result .failure(RuntimeException (" No API key — run ./set_key.sh" ))
122132
133+ Log .i(TAG , " Querying Anthropic: '${prompt.take(50 )} '" )
123134 try {
124- val process = ProcessBuilder (
125- binaryFile.absolutePath,
126- " agent" ,
127- " --message" , prompt,
128- " --output" , " plain" ,
129- " --config" , configFile.absolutePath
130- )
131- .directory(filesDir)
132- .apply {
133- environment().apply {
134- put(" ANTHROPIC_API_KEY" , apiKey)
135- put(" HOME" , homeDir.absolutePath)
136- // filesDir has our 'curl' binary — NullClaw calls curl as subprocess
137- put(" PATH" , " ${filesDir.absolutePath} :$nativeLibDir :/system/bin:/system/xbin" )
138- // CA bundle for curl's HTTPS calls
139- if (caBundleFile.exists()) {
140- put(" CURL_CA_BUNDLE" , caBundleFile.absolutePath)
141- put(" SSL_CERT_FILE" , caBundleFile.absolutePath)
142- }
143- }
144- }
145- .start()
146-
147- // Fix #7: read stdout and stderr concurrently
148- Log .i(TAG , " NullClaw running: prompt='${prompt.take(50 )} ' binary=${binaryFile.absolutePath} home=${homeDir.absolutePath} config=${nullclawConfigFile.exists()} " )
149-
150- var output = " "
151- var error = " "
152- val stdoutThread = Thread { output = process.inputStream.bufferedReader().readText() }
153- val stderrThread = Thread { error = process.errorStream.bufferedReader().readText() }
154- stdoutThread.start()
155- stderrThread.start()
156- val exit = process.waitFor()
157- stdoutThread.join()
158- stderrThread.join()
159-
160- Log .i(TAG , " NullClaw exit=$exit output='${output.take(100 )} ' stderr='${error.take(200 )} '" )
161-
162- if (exit != 0 ) {
163- Log .e(TAG , " NullClaw failed exit $exit : $error " )
164- Result .failure(RuntimeException (error.take(120 )))
165- } else if (output.isBlank()) {
166- Log .e(TAG , " NullClaw returned empty output. stderr: $error " )
167- Result .failure(RuntimeException (if (error.isNotBlank()) error.take(120 ) else " Empty response" ))
168- } else {
169- Result .success(output.trim())
135+ val body = JSONObject ().apply {
136+ put(" model" , " claude-opus-4-6" )
137+ put(" max_tokens" , 150 )
138+ put(" system" , SYSTEM_PROMPT )
139+ put(" messages" , JSONArray ().apply {
140+ put(JSONObject ().apply {
141+ put(" role" , " user" )
142+ put(" content" , prompt)
143+ })
144+ })
145+ }.toString()
146+
147+ val url = URL (" https://api.anthropic.com/v1/messages" )
148+ val conn = url.openConnection() as HttpURLConnection
149+ conn.requestMethod = " POST"
150+ conn.setRequestProperty(" Content-Type" , " application/json" )
151+ conn.setRequestProperty(" x-api-key" , apiKey)
152+ conn.setRequestProperty(" anthropic-version" , " 2023-06-01" )
153+ conn.connectTimeout = 30_000
154+ conn.readTimeout = 30_000
155+ conn.doOutput = true
156+
157+ OutputStreamWriter (conn.outputStream).use { it.write(body) }
158+
159+ val responseCode = conn.responseCode
160+ val responseBody = if (responseCode == 200 )
161+ conn.inputStream.bufferedReader().readText()
162+ else
163+ conn.errorStream?.bufferedReader()?.readText() ? : " HTTP $responseCode "
164+
165+ Log .i(TAG , " Anthropic response code=$responseCode " )
166+
167+ if (responseCode != 200 ) {
168+ Log .e(TAG , " Anthropic error: $responseBody " )
169+ return @withContext Result .failure(RuntimeException (" API error $responseCode " ))
170170 }
171+
172+ val text = JSONObject (responseBody)
173+ .getJSONArray(" content" )
174+ .getJSONObject(0 )
175+ .getString(" text" )
176+ .trim()
177+
178+ Log .i(TAG , " Response: '${text.take(80 )} '" )
179+ Result .success(text)
171180 } catch (e: Exception ) {
172- Log .e(TAG , " ClawRunner error" , e)
181+ Log .e(TAG , " Query error" , e)
173182 Result .failure(e)
174183 }
175184 }
0 commit comments