@zhigang1992/happy-cli 0.12.3 → 0.12.4

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.
@@ -41,7 +41,7 @@ function _interopNamespaceDefault(e) {
41
41
  var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
42
42
 
43
43
  var name = "@zhigang1992/happy-cli";
44
- var version = "0.12.2";
44
+ var version = "0.12.4";
45
45
  var description = "Mobile and Web client for Claude Code and Codex";
46
46
  var author = "Kirill Dubovitskiy";
47
47
  var license = "MIT";
@@ -332,6 +332,31 @@ function decryptWithDataKey(bundle, dataKey) {
332
332
  return null;
333
333
  }
334
334
  }
335
+ function decryptBlobWithDataKey(bundle, dataKey) {
336
+ if (bundle.length < 1) {
337
+ return null;
338
+ }
339
+ if (bundle[0] !== 0) {
340
+ return null;
341
+ }
342
+ if (bundle.length < 12 + 16 + 1) {
343
+ return null;
344
+ }
345
+ const nonce = bundle.slice(1, 13);
346
+ const authTag = bundle.slice(bundle.length - 16);
347
+ const ciphertext = bundle.slice(13, bundle.length - 16);
348
+ try {
349
+ const decipher = node_crypto.createDecipheriv("aes-256-gcm", dataKey, nonce);
350
+ decipher.setAuthTag(authTag);
351
+ const decrypted = Buffer.concat([
352
+ decipher.update(ciphertext),
353
+ decipher.final()
354
+ ]);
355
+ return new Uint8Array(decrypted);
356
+ } catch (error) {
357
+ return null;
358
+ }
359
+ }
335
360
  function encrypt(key, variant, data) {
336
361
  if (variant === "legacy") {
337
362
  return encryptLegacy(data, key);
@@ -873,12 +898,27 @@ z.z.object({
873
898
  agentStateVersion: z.z.number()
874
899
  })
875
900
  });
901
+ const TextContentSchema = z.z.object({
902
+ type: z.z.literal("text"),
903
+ text: z.z.string()
904
+ });
905
+ const ImageRefContentSchema = z.z.object({
906
+ type: z.z.literal("image_ref"),
907
+ blobId: z.z.string(),
908
+ mimeType: z.z.enum(["image/jpeg", "image/png", "image/gif", "image/webp"]),
909
+ width: z.z.number().optional(),
910
+ height: z.z.number().optional()
911
+ });
912
+ const ContentBlockSchema = z.z.union([TextContentSchema, ImageRefContentSchema]);
913
+ const UserMessageContentSchema = z.z.union([
914
+ TextContentSchema,
915
+ // Legacy: single text content
916
+ z.z.array(ContentBlockSchema)
917
+ // New: array of content blocks
918
+ ]);
876
919
  const UserMessageSchema = z.z.object({
877
920
  role: z.z.literal("user"),
878
- content: z.z.object({
879
- type: z.z.literal("text"),
880
- text: z.z.string()
881
- }),
921
+ content: UserMessageContentSchema,
882
922
  localKey: z.z.string().optional(),
883
923
  // Mobile messages include this
884
924
  meta: MessageMetaSchema.optional()
@@ -990,12 +1030,29 @@ class RpcHandlerManager {
990
1030
  encryptionVariant;
991
1031
  logger;
992
1032
  socket = null;
1033
+ sessionContext = null;
993
1034
  constructor(config) {
994
1035
  this.scopePrefix = config.scopePrefix;
995
1036
  this.encryptionKey = config.encryptionKey;
996
1037
  this.encryptionVariant = config.encryptionVariant;
997
1038
  this.logger = config.logger || ((msg, data) => logger.debug(msg, data));
998
1039
  }
1040
+ /**
1041
+ * Set the session context (path and environment)
1042
+ * This should be called after direnv environment is loaded
1043
+ * @param context - The session context with path and environment
1044
+ */
1045
+ setSessionContext(context) {
1046
+ this.sessionContext = context;
1047
+ this.logger("[RPC] Session context set", { path: context.path, envVarCount: Object.keys(context.env).length });
1048
+ }
1049
+ /**
1050
+ * Get the current session context
1051
+ * @returns The session context or null if not set
1052
+ */
1053
+ getSessionContext() {
1054
+ return this.sessionContext;
1055
+ }
999
1056
  /**
1000
1057
  * Register an RPC handler for a specific method
1001
1058
  * @param method - The method name (without prefix)
@@ -1073,7 +1130,7 @@ class RpcHandlerManager {
1073
1130
  }
1074
1131
  }
1075
1132
 
1076
- const __dirname$1 = path.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-CllU28mx.cjs', document.baseURI).href))));
1133
+ const __dirname$1 = path.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-Q-euvEmG.cjs', document.baseURI).href))));
1077
1134
  function projectPath() {
1078
1135
  const path$1 = path.resolve(__dirname$1, "..");
1079
1136
  return path$1;
@@ -1171,10 +1228,13 @@ function registerCommonHandlers(rpcHandlerManager) {
1171
1228
  rpcHandlerManager.registerHandler("bash", async (data) => {
1172
1229
  logger.debug("Shell command request:", data.command);
1173
1230
  try {
1231
+ const sessionContext = rpcHandlerManager.getSessionContext();
1174
1232
  const options = {
1175
1233
  cwd: data.cwd,
1176
- timeout: data.timeout || 3e4
1234
+ timeout: data.timeout || 3e4,
1177
1235
  // Default 30 seconds timeout
1236
+ // Use session environment if available, otherwise inherit process.env
1237
+ env: sessionContext?.env ?? process.env
1178
1238
  };
1179
1239
  const { stdout, stderr } = await execAsync(data.command, options);
1180
1240
  return {
@@ -1704,6 +1764,64 @@ class ApiSessionClient extends node_events.EventEmitter {
1704
1764
  logger.debug("[API] socket.close() called");
1705
1765
  this.socket.close();
1706
1766
  }
1767
+ /**
1768
+ * Download and decrypt a blob from the server
1769
+ * @param blobId - The blob ID to download
1770
+ * @returns The decrypted binary data and metadata, or null if download/decryption fails
1771
+ */
1772
+ async downloadBlob(blobId) {
1773
+ try {
1774
+ const response = await axios.get(
1775
+ `${configuration.serverUrl}/v1/sessions/${this.sessionId}/blobs/${blobId}`,
1776
+ {
1777
+ headers: {
1778
+ "Authorization": `Bearer ${this.token}`
1779
+ },
1780
+ responseType: "arraybuffer"
1781
+ }
1782
+ );
1783
+ const encryptedData = new Uint8Array(response.data);
1784
+ const mimeType = response.headers["x-blob-mimetype"];
1785
+ const originalSize = parseInt(response.headers["x-blob-size"], 10);
1786
+ if (this.encryptionVariant !== "dataKey") {
1787
+ logger.debug("[API] Cannot decrypt blob: session uses legacy encryption");
1788
+ return null;
1789
+ }
1790
+ const decryptedData = decryptBlobWithDataKey(encryptedData, this.encryptionKey);
1791
+ if (!decryptedData) {
1792
+ logger.debug("[API] Failed to decrypt blob");
1793
+ return null;
1794
+ }
1795
+ return {
1796
+ data: decryptedData,
1797
+ mimeType,
1798
+ size: originalSize
1799
+ };
1800
+ } catch (error) {
1801
+ logger.debug("[API] Failed to download blob:", error);
1802
+ return null;
1803
+ }
1804
+ }
1805
+ /**
1806
+ * Convert an image_ref content block to a Claude API image content block
1807
+ * Downloads, decrypts, and base64 encodes the image
1808
+ * @param imageRef - The image reference content block
1809
+ * @returns Claude API image content block, or null if conversion fails
1810
+ */
1811
+ async resolveImageRef(imageRef) {
1812
+ const blob = await this.downloadBlob(imageRef.blobId);
1813
+ if (!blob) {
1814
+ return null;
1815
+ }
1816
+ return {
1817
+ type: "image",
1818
+ source: {
1819
+ type: "base64",
1820
+ media_type: blob.mimeType,
1821
+ data: Buffer.from(blob.data).toString("base64")
1822
+ }
1823
+ };
1824
+ }
1707
1825
  }
1708
1826
 
1709
1827
  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.4",
4
4
  "description": "Mobile and Web client for Claude Code and Codex",
5
5
  "author": "Kirill Dubovitskiy",
6
6
  "license": "MIT",