@zhigang1992/happy-cli 0.12.3 → 0.12.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,7 +20,7 @@ import { fileURLToPath } from 'url';
20
20
  import { Expo } from 'expo-server-sdk';
21
21
 
22
22
  var name = "@zhigang1992/happy-cli";
23
- var version = "0.12.2";
23
+ var version = "0.12.5";
24
24
  var description = "Mobile and Web client for Claude Code and Codex";
25
25
  var author = "Kirill Dubovitskiy";
26
26
  var license = "MIT";
@@ -311,6 +311,43 @@ function decryptWithDataKey(bundle, dataKey) {
311
311
  return null;
312
312
  }
313
313
  }
314
+ function decryptBlobWithDataKey(bundle, dataKey) {
315
+ if (bundle.length < 1) {
316
+ return null;
317
+ }
318
+ if (bundle[0] !== 0) {
319
+ return null;
320
+ }
321
+ if (bundle.length < 12 + 16 + 1) {
322
+ return null;
323
+ }
324
+ const nonce = bundle.slice(1, 13);
325
+ const authTag = bundle.slice(bundle.length - 16);
326
+ const ciphertext = bundle.slice(13, bundle.length - 16);
327
+ try {
328
+ const decipher = createDecipheriv("aes-256-gcm", dataKey, nonce);
329
+ decipher.setAuthTag(authTag);
330
+ const decrypted = Buffer.concat([
331
+ decipher.update(ciphertext),
332
+ decipher.final()
333
+ ]);
334
+ return new Uint8Array(decrypted);
335
+ } catch (error) {
336
+ return null;
337
+ }
338
+ }
339
+ function decryptBlobWithSecretBox(bundle, secret) {
340
+ if (bundle.length < 1 + tweetnacl.secretbox.nonceLength) {
341
+ return null;
342
+ }
343
+ if (bundle[0] !== 0) {
344
+ return null;
345
+ }
346
+ const nonce = bundle.slice(1, 1 + tweetnacl.secretbox.nonceLength);
347
+ const ciphertext = bundle.slice(1 + tweetnacl.secretbox.nonceLength);
348
+ const decrypted = tweetnacl.secretbox.open(ciphertext, nonce, secret);
349
+ return decrypted || null;
350
+ }
314
351
  function encrypt(key, variant, data) {
315
352
  if (variant === "legacy") {
316
353
  return encryptLegacy(data, key);
@@ -325,6 +362,13 @@ function decrypt(key, variant, data) {
325
362
  return decryptWithDataKey(data, key);
326
363
  }
327
364
  }
365
+ function decryptBlob(key, variant, bundle) {
366
+ if (variant === "legacy") {
367
+ return decryptBlobWithSecretBox(bundle, key);
368
+ } else {
369
+ return decryptBlobWithDataKey(bundle, key);
370
+ }
371
+ }
328
372
  function authChallenge(secret) {
329
373
  const keypair = tweetnacl.sign.keyPair.fromSeed(secret);
330
374
  const challenge = getRandomBytes(32);
@@ -852,12 +896,27 @@ z$1.object({
852
896
  agentStateVersion: z$1.number()
853
897
  })
854
898
  });
899
+ const TextContentSchema = z$1.object({
900
+ type: z$1.literal("text"),
901
+ text: z$1.string()
902
+ });
903
+ const ImageRefContentSchema = z$1.object({
904
+ type: z$1.literal("image_ref"),
905
+ blobId: z$1.string(),
906
+ mimeType: z$1.enum(["image/jpeg", "image/png", "image/gif", "image/webp"]),
907
+ width: z$1.number().optional(),
908
+ height: z$1.number().optional()
909
+ });
910
+ const ContentBlockSchema = z$1.union([TextContentSchema, ImageRefContentSchema]);
911
+ const UserMessageContentSchema = z$1.union([
912
+ TextContentSchema,
913
+ // Legacy: single text content
914
+ z$1.array(ContentBlockSchema)
915
+ // New: array of content blocks
916
+ ]);
855
917
  const UserMessageSchema = z$1.object({
856
918
  role: z$1.literal("user"),
857
- content: z$1.object({
858
- type: z$1.literal("text"),
859
- text: z$1.string()
860
- }),
919
+ content: UserMessageContentSchema,
861
920
  localKey: z$1.string().optional(),
862
921
  // Mobile messages include this
863
922
  meta: MessageMetaSchema.optional()
@@ -969,12 +1028,29 @@ class RpcHandlerManager {
969
1028
  encryptionVariant;
970
1029
  logger;
971
1030
  socket = null;
1031
+ sessionContext = null;
972
1032
  constructor(config) {
973
1033
  this.scopePrefix = config.scopePrefix;
974
1034
  this.encryptionKey = config.encryptionKey;
975
1035
  this.encryptionVariant = config.encryptionVariant;
976
1036
  this.logger = config.logger || ((msg, data) => logger.debug(msg, data));
977
1037
  }
1038
+ /**
1039
+ * Set the session context (path and environment)
1040
+ * This should be called after direnv environment is loaded
1041
+ * @param context - The session context with path and environment
1042
+ */
1043
+ setSessionContext(context) {
1044
+ this.sessionContext = context;
1045
+ this.logger("[RPC] Session context set", { path: context.path, envVarCount: Object.keys(context.env).length });
1046
+ }
1047
+ /**
1048
+ * Get the current session context
1049
+ * @returns The session context or null if not set
1050
+ */
1051
+ getSessionContext() {
1052
+ return this.sessionContext;
1053
+ }
978
1054
  /**
979
1055
  * Register an RPC handler for a specific method
980
1056
  * @param method - The method name (without prefix)
@@ -1150,10 +1226,13 @@ function registerCommonHandlers(rpcHandlerManager) {
1150
1226
  rpcHandlerManager.registerHandler("bash", async (data) => {
1151
1227
  logger.debug("Shell command request:", data.command);
1152
1228
  try {
1229
+ const sessionContext = rpcHandlerManager.getSessionContext();
1153
1230
  const options = {
1154
1231
  cwd: data.cwd,
1155
- timeout: data.timeout || 3e4
1232
+ timeout: data.timeout || 3e4,
1156
1233
  // Default 30 seconds timeout
1234
+ // Use session environment if available, otherwise inherit process.env
1235
+ env: sessionContext?.env ?? process.env
1157
1236
  };
1158
1237
  const { stdout, stderr } = await execAsync(data.command, options);
1159
1238
  return {
@@ -1683,6 +1762,60 @@ class ApiSessionClient extends EventEmitter {
1683
1762
  logger.debug("[API] socket.close() called");
1684
1763
  this.socket.close();
1685
1764
  }
1765
+ /**
1766
+ * Download and decrypt a blob from the server
1767
+ * @param blobId - The blob ID to download
1768
+ * @returns The decrypted binary data and metadata, or null if download/decryption fails
1769
+ */
1770
+ async downloadBlob(blobId) {
1771
+ try {
1772
+ const response = await axios.get(
1773
+ `${configuration.serverUrl}/v1/sessions/${this.sessionId}/blobs/${blobId}`,
1774
+ {
1775
+ headers: {
1776
+ "Authorization": `Bearer ${this.token}`
1777
+ },
1778
+ responseType: "arraybuffer"
1779
+ }
1780
+ );
1781
+ const encryptedData = new Uint8Array(response.data);
1782
+ const mimeType = response.headers["x-blob-mimetype"];
1783
+ const originalSize = parseInt(response.headers["x-blob-size"], 10);
1784
+ const decryptedData = decryptBlob(this.encryptionKey, this.encryptionVariant, encryptedData);
1785
+ if (!decryptedData) {
1786
+ logger.debug("[API] Failed to decrypt blob");
1787
+ return null;
1788
+ }
1789
+ return {
1790
+ data: decryptedData,
1791
+ mimeType,
1792
+ size: originalSize
1793
+ };
1794
+ } catch (error) {
1795
+ logger.debug("[API] Failed to download blob:", error);
1796
+ return null;
1797
+ }
1798
+ }
1799
+ /**
1800
+ * Convert an image_ref content block to a Claude API image content block
1801
+ * Downloads, decrypts, and base64 encodes the image
1802
+ * @param imageRef - The image reference content block
1803
+ * @returns Claude API image content block, or null if conversion fails
1804
+ */
1805
+ async resolveImageRef(imageRef) {
1806
+ const blob = await this.downloadBlob(imageRef.blobId);
1807
+ if (!blob) {
1808
+ return null;
1809
+ }
1810
+ return {
1811
+ type: "image",
1812
+ source: {
1813
+ type: "base64",
1814
+ media_type: blob.mimeType,
1815
+ data: Buffer.from(blob.data).toString("base64")
1816
+ }
1817
+ };
1818
+ }
1686
1819
  }
1687
1820
 
1688
1821
  class ApiMachineClient {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhigang1992/happy-cli",
3
- "version": "0.12.3",
3
+ "version": "0.12.5",
4
4
  "description": "Mobile and Web client for Claude Code and Codex",
5
5
  "author": "Kirill Dubovitskiy",
6
6
  "license": "MIT",