Skip to content

Commit 69ddb81

Browse files
committed
fix: call Anthropic API directly from Kotlin — Wear OS blocks subprocess networking; drop bundled curl (APK now 71MB)
1 parent 79842f3 commit 69ddb81

2 files changed

Lines changed: 58 additions & 49 deletions

File tree

app/src/main/java/com/thinkoff/clawwatch/ClawRunner.kt

Lines changed: 58 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import android.content.Context
44
import android.util.Log
55
import kotlinx.coroutines.Dispatchers
66
import kotlinx.coroutines.withContext
7+
import org.json.JSONArray
8+
import org.json.JSONObject
79
import 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
}
-3.61 MB
Binary file not shown.

0 commit comments

Comments
 (0)