happy-coder 0.11.2 → 0.12.0

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.
@@ -21,7 +21,7 @@ import { platform } from 'os';
21
21
  import { Expo } from 'expo-server-sdk';
22
22
 
23
23
  var name = "happy-coder";
24
- var version = "0.11.2";
24
+ var version = "0.12.0";
25
25
  var description = "Mobile and Web client for Claude Code and Codex";
26
26
  var author = "Kirill Dubovitskiy";
27
27
  var license = "MIT";
@@ -80,7 +80,9 @@ var scripts = {
80
80
  typecheck: "tsc --noEmit",
81
81
  build: "shx rm -rf dist && npx tsc --noEmit && pkgroll",
82
82
  test: "yarn build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
83
+ "test:win": "yarn build && npx vitest run",
83
84
  start: "yarn build && ./bin/happy.mjs",
85
+ "start:win": "yarn build && node ./bin/happy.mjs",
84
86
  dev: "tsx src/index.ts",
85
87
  "dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
86
88
  "dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
@@ -89,32 +91,30 @@ var scripts = {
89
91
  postinstall: "node scripts/unpack-tools.cjs"
90
92
  };
91
93
  var dependencies = {
92
- "@anthropic-ai/claude-code": "2.0.14",
93
- "@anthropic-ai/sdk": "0.65.0",
94
- "@modelcontextprotocol/sdk": "^1.15.1",
94
+ "@modelcontextprotocol/sdk": "^1.22.0",
95
95
  "@stablelib/base64": "^2.0.1",
96
96
  "@stablelib/hex": "^2.0.1",
97
97
  "@types/cross-spawn": "^6.0.6",
98
- "@types/http-proxy": "^1.17.16",
98
+ "@types/http-proxy": "^1.17.17",
99
99
  "@types/ps-list": "^6.2.1",
100
100
  "@types/qrcode-terminal": "^0.12.2",
101
- "@types/react": "^19.1.9",
101
+ "@types/react": "^19.2.7",
102
102
  "@types/tmp": "^0.2.6",
103
- axios: "^1.10.0",
104
- chalk: "^5.4.1",
103
+ axios: "^1.13.2",
104
+ chalk: "^5.6.2",
105
105
  "cross-spawn": "^7.0.6",
106
106
  "expo-server-sdk": "^3.15.0",
107
- fastify: "^5.5.0",
107
+ fastify: "^5.6.2",
108
108
  "fastify-type-provider-zod": "4.0.2",
109
109
  "http-proxy": "^1.18.1",
110
110
  "http-proxy-middleware": "^3.0.5",
111
- ink: "^6.1.0",
111
+ ink: "^6.5.1",
112
112
  open: "^10.2.0",
113
113
  "ps-list": "^8.1.1",
114
114
  "qrcode-terminal": "^0.12.0",
115
- react: "^19.1.1",
115
+ react: "^19.2.0",
116
116
  "socket.io-client": "^4.8.1",
117
- tar: "^7.4.3",
117
+ tar: "^7.5.2",
118
118
  tmp: "^0.2.5",
119
119
  tweetnacl: "^1.0.3",
120
120
  zod: "^3.23.8"
@@ -122,15 +122,15 @@ var dependencies = {
122
122
  var devDependencies = {
123
123
  "@eslint/compat": "^1",
124
124
  "@types/node": ">=20",
125
- "cross-env": "^10.0.0",
125
+ "cross-env": "^10.1.0",
126
126
  dotenv: "^16.6.1",
127
127
  eslint: "^9",
128
128
  "eslint-config-prettier": "^10",
129
129
  pkgroll: "^2.14.2",
130
- "release-it": "^19.0.4",
130
+ "release-it": "^19.0.6",
131
131
  shx: "^0.3.3",
132
132
  "ts-node": "^10",
133
- tsx: "^4.20.3",
133
+ tsx: "^4.20.6",
134
134
  typescript: "^5",
135
135
  vitest: "^3.2.4"
136
136
  };
@@ -1070,10 +1070,28 @@ function run(args, options) {
1070
1070
  });
1071
1071
  }
1072
1072
 
1073
+ function validatePath(targetPath, workingDirectory) {
1074
+ const resolvedTarget = resolve(workingDirectory, targetPath);
1075
+ const resolvedWorkingDir = resolve(workingDirectory);
1076
+ if (!resolvedTarget.startsWith(resolvedWorkingDir + "/") && resolvedTarget !== resolvedWorkingDir) {
1077
+ return {
1078
+ valid: false,
1079
+ error: `Access denied: Path '${targetPath}' is outside the working directory`
1080
+ };
1081
+ }
1082
+ return { valid: true };
1083
+ }
1084
+
1073
1085
  const execAsync = promisify(exec);
1074
- function registerCommonHandlers(rpcHandlerManager) {
1086
+ function registerCommonHandlers(rpcHandlerManager, workingDirectory) {
1075
1087
  rpcHandlerManager.registerHandler("bash", async (data) => {
1076
1088
  logger.debug("Shell command request:", data.command);
1089
+ if (data.cwd) {
1090
+ const validation = validatePath(data.cwd, workingDirectory);
1091
+ if (!validation.valid) {
1092
+ return { success: false, error: validation.error };
1093
+ }
1094
+ }
1077
1095
  try {
1078
1096
  const options = {
1079
1097
  cwd: data.cwd,
@@ -1109,6 +1127,10 @@ function registerCommonHandlers(rpcHandlerManager) {
1109
1127
  });
1110
1128
  rpcHandlerManager.registerHandler("readFile", async (data) => {
1111
1129
  logger.debug("Read file request:", data.path);
1130
+ const validation = validatePath(data.path, workingDirectory);
1131
+ if (!validation.valid) {
1132
+ return { success: false, error: validation.error };
1133
+ }
1112
1134
  try {
1113
1135
  const buffer = await readFile$1(data.path);
1114
1136
  const content = buffer.toString("base64");
@@ -1120,6 +1142,10 @@ function registerCommonHandlers(rpcHandlerManager) {
1120
1142
  });
1121
1143
  rpcHandlerManager.registerHandler("writeFile", async (data) => {
1122
1144
  logger.debug("Write file request:", data.path);
1145
+ const validation = validatePath(data.path, workingDirectory);
1146
+ if (!validation.valid) {
1147
+ return { success: false, error: validation.error };
1148
+ }
1123
1149
  try {
1124
1150
  if (data.expectedHash !== null && data.expectedHash !== void 0) {
1125
1151
  try {
@@ -1166,6 +1192,10 @@ function registerCommonHandlers(rpcHandlerManager) {
1166
1192
  });
1167
1193
  rpcHandlerManager.registerHandler("listDirectory", async (data) => {
1168
1194
  logger.debug("List directory request:", data.path);
1195
+ const validation = validatePath(data.path, workingDirectory);
1196
+ if (!validation.valid) {
1197
+ return { success: false, error: validation.error };
1198
+ }
1169
1199
  try {
1170
1200
  const entries = await readdir(data.path, { withFileTypes: true });
1171
1201
  const directoryEntries = await Promise.all(
@@ -1207,6 +1237,10 @@ function registerCommonHandlers(rpcHandlerManager) {
1207
1237
  });
1208
1238
  rpcHandlerManager.registerHandler("getDirectoryTree", async (data) => {
1209
1239
  logger.debug("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
1240
+ const validation = validatePath(data.path, workingDirectory);
1241
+ if (!validation.valid) {
1242
+ return { success: false, error: validation.error };
1243
+ }
1210
1244
  async function buildTree(path, name, currentDepth) {
1211
1245
  try {
1212
1246
  const stats = await stat$1(path);
@@ -1263,6 +1297,12 @@ function registerCommonHandlers(rpcHandlerManager) {
1263
1297
  });
1264
1298
  rpcHandlerManager.registerHandler("ripgrep", async (data) => {
1265
1299
  logger.debug("Ripgrep request with args:", data.args, "cwd:", data.cwd);
1300
+ if (data.cwd) {
1301
+ const validation = validatePath(data.cwd, workingDirectory);
1302
+ if (!validation.valid) {
1303
+ return { success: false, error: validation.error };
1304
+ }
1305
+ }
1266
1306
  try {
1267
1307
  const result = await run$1(data.args, { cwd: data.cwd });
1268
1308
  return {
@@ -1281,6 +1321,12 @@ function registerCommonHandlers(rpcHandlerManager) {
1281
1321
  });
1282
1322
  rpcHandlerManager.registerHandler("difftastic", async (data) => {
1283
1323
  logger.debug("Difftastic request with args:", data.args, "cwd:", data.cwd);
1324
+ if (data.cwd) {
1325
+ const validation = validatePath(data.cwd, workingDirectory);
1326
+ if (!validation.valid) {
1327
+ return { success: false, error: validation.error };
1328
+ }
1329
+ }
1284
1330
  try {
1285
1331
  const result = await run(data.args, { cwd: data.cwd });
1286
1332
  return {
@@ -1330,7 +1376,7 @@ class ApiSessionClient extends EventEmitter {
1330
1376
  encryptionVariant: this.encryptionVariant,
1331
1377
  logger: (msg, data) => logger.debug(msg, data)
1332
1378
  });
1333
- registerCommonHandlers(this.rpcHandlerManager);
1379
+ registerCommonHandlers(this.rpcHandlerManager, this.metadata.path);
1334
1380
  this.socket = io(configuration.serverUrl, {
1335
1381
  auth: {
1336
1382
  token: this.token,
@@ -1446,7 +1492,7 @@ class ApiSessionClient extends EventEmitter {
1446
1492
  sid: this.sessionId,
1447
1493
  message: encrypted
1448
1494
  });
1449
- if (body.type === "assistant" && body.message.usage) {
1495
+ if (body.type === "assistant" && body.message?.usage) {
1450
1496
  try {
1451
1497
  this.sendUsageData(body.message.usage);
1452
1498
  } catch (error) {
@@ -1620,7 +1666,7 @@ class ApiMachineClient {
1620
1666
  encryptionVariant: this.machine.encryptionVariant,
1621
1667
  logger: (msg, data) => logger.debug(msg, data)
1622
1668
  });
1623
- registerCommonHandlers(this.rpcHandlerManager);
1669
+ registerCommonHandlers(this.rpcHandlerManager, process.cwd());
1624
1670
  }
1625
1671
  socket;
1626
1672
  keepAliveInterval = null;
@@ -1836,7 +1882,7 @@ class PushNotificationClient {
1836
1882
  );
1837
1883
  logger.debug(`Fetched ${response.data.tokens.length} push tokens`);
1838
1884
  response.data.tokens.forEach((token, index) => {
1839
- logger.debug(`[PUSH] Token ${index + 1}: id=${token.id}, token=${token.token}, created=${new Date(token.createdAt).toISOString()}, updated=${new Date(token.updatedAt).toISOString()}`);
1885
+ logger.debug(`[PUSH] Token ${index + 1}: id=${token.id}, created=${new Date(token.createdAt).toISOString()}, updated=${new Date(token.updatedAt).toISOString()}`);
1840
1886
  });
1841
1887
  return response.data.tokens;
1842
1888
  } catch (error) {
@@ -1910,14 +1956,14 @@ class PushNotificationClient {
1910
1956
  const tokens = await this.fetchPushTokens();
1911
1957
  logger.debug(`[PUSH] Fetched ${tokens.length} push tokens`);
1912
1958
  tokens.forEach((token, index) => {
1913
- logger.debug(`[PUSH] Using token ${index + 1}: id=${token.id}, token=${token.token}`);
1959
+ logger.debug(`[PUSH] Using token ${index + 1}: id=${token.id}`);
1914
1960
  });
1915
1961
  if (tokens.length === 0) {
1916
1962
  logger.debug("No push tokens found for user");
1917
1963
  return;
1918
1964
  }
1919
1965
  const messages = tokens.map((token, index) => {
1920
- logger.debug(`[PUSH] Creating message ${index + 1} for token: ${token.token}`);
1966
+ logger.debug(`[PUSH] Creating message ${index + 1} for token`);
1921
1967
  return {
1922
1968
  to: token.token,
1923
1969
  title,
@@ -2114,17 +2160,16 @@ const RawJSONLinesSchema = z$1.discriminatedUnion("type", [
2114
2160
  // Used in sessionScanner.ts
2115
2161
  }).passthrough()
2116
2162
  }).passthrough(),
2117
- // Assistant message - validates message object with usage and content
2163
+ // Assistant message - only validates uuid and type
2164
+ // message object is optional to handle synthetic error messages (isApiErrorMessage: true)
2165
+ // which may have different structure than normal assistant messages
2118
2166
  z$1.object({
2119
2167
  uuid: z$1.string(),
2120
2168
  type: z$1.literal("assistant"),
2121
2169
  message: z$1.object({
2122
- // Entire message used in getMessageKey()
2123
- usage: UsageSchema.optional(),
2170
+ usage: UsageSchema.optional()
2124
2171
  // Used in apiSession.ts
2125
- content: z$1.any()
2126
- // Used in tests
2127
- }).passthrough()
2172
+ }).passthrough().optional()
2128
2173
  }).passthrough(),
2129
2174
  // Summary message - validates summary and leafUuid
2130
2175
  z$1.object({
@@ -42,7 +42,7 @@ function _interopNamespaceDefault(e) {
42
42
  var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
43
43
 
44
44
  var name = "happy-coder";
45
- var version = "0.11.2";
45
+ var version = "0.12.0";
46
46
  var description = "Mobile and Web client for Claude Code and Codex";
47
47
  var author = "Kirill Dubovitskiy";
48
48
  var license = "MIT";
@@ -101,7 +101,9 @@ var scripts = {
101
101
  typecheck: "tsc --noEmit",
102
102
  build: "shx rm -rf dist && npx tsc --noEmit && pkgroll",
103
103
  test: "yarn build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
104
+ "test:win": "yarn build && npx vitest run",
104
105
  start: "yarn build && ./bin/happy.mjs",
106
+ "start:win": "yarn build && node ./bin/happy.mjs",
105
107
  dev: "tsx src/index.ts",
106
108
  "dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
107
109
  "dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
@@ -110,32 +112,30 @@ var scripts = {
110
112
  postinstall: "node scripts/unpack-tools.cjs"
111
113
  };
112
114
  var dependencies = {
113
- "@anthropic-ai/claude-code": "2.0.14",
114
- "@anthropic-ai/sdk": "0.65.0",
115
- "@modelcontextprotocol/sdk": "^1.15.1",
115
+ "@modelcontextprotocol/sdk": "^1.22.0",
116
116
  "@stablelib/base64": "^2.0.1",
117
117
  "@stablelib/hex": "^2.0.1",
118
118
  "@types/cross-spawn": "^6.0.6",
119
- "@types/http-proxy": "^1.17.16",
119
+ "@types/http-proxy": "^1.17.17",
120
120
  "@types/ps-list": "^6.2.1",
121
121
  "@types/qrcode-terminal": "^0.12.2",
122
- "@types/react": "^19.1.9",
122
+ "@types/react": "^19.2.7",
123
123
  "@types/tmp": "^0.2.6",
124
- axios: "^1.10.0",
125
- chalk: "^5.4.1",
124
+ axios: "^1.13.2",
125
+ chalk: "^5.6.2",
126
126
  "cross-spawn": "^7.0.6",
127
127
  "expo-server-sdk": "^3.15.0",
128
- fastify: "^5.5.0",
128
+ fastify: "^5.6.2",
129
129
  "fastify-type-provider-zod": "4.0.2",
130
130
  "http-proxy": "^1.18.1",
131
131
  "http-proxy-middleware": "^3.0.5",
132
- ink: "^6.1.0",
132
+ ink: "^6.5.1",
133
133
  open: "^10.2.0",
134
134
  "ps-list": "^8.1.1",
135
135
  "qrcode-terminal": "^0.12.0",
136
- react: "^19.1.1",
136
+ react: "^19.2.0",
137
137
  "socket.io-client": "^4.8.1",
138
- tar: "^7.4.3",
138
+ tar: "^7.5.2",
139
139
  tmp: "^0.2.5",
140
140
  tweetnacl: "^1.0.3",
141
141
  zod: "^3.23.8"
@@ -143,15 +143,15 @@ var dependencies = {
143
143
  var devDependencies = {
144
144
  "@eslint/compat": "^1",
145
145
  "@types/node": ">=20",
146
- "cross-env": "^10.0.0",
146
+ "cross-env": "^10.1.0",
147
147
  dotenv: "^16.6.1",
148
148
  eslint: "^9",
149
149
  "eslint-config-prettier": "^10",
150
150
  pkgroll: "^2.14.2",
151
- "release-it": "^19.0.4",
151
+ "release-it": "^19.0.6",
152
152
  shx: "^0.3.3",
153
153
  "ts-node": "^10",
154
- tsx: "^4.20.3",
154
+ tsx: "^4.20.6",
155
155
  typescript: "^5",
156
156
  vitest: "^3.2.4"
157
157
  };
@@ -1019,7 +1019,7 @@ class RpcHandlerManager {
1019
1019
  }
1020
1020
  }
1021
1021
 
1022
- 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-Bg43e3vc.cjs', document.baseURI).href))));
1022
+ 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-CO3A_kFm.cjs', document.baseURI).href))));
1023
1023
  function projectPath() {
1024
1024
  const path$1 = path.resolve(__dirname$1, "..");
1025
1025
  return path$1;
@@ -1091,10 +1091,28 @@ function run(args, options) {
1091
1091
  });
1092
1092
  }
1093
1093
 
1094
+ function validatePath(targetPath, workingDirectory) {
1095
+ const resolvedTarget = path.resolve(workingDirectory, targetPath);
1096
+ const resolvedWorkingDir = path.resolve(workingDirectory);
1097
+ if (!resolvedTarget.startsWith(resolvedWorkingDir + "/") && resolvedTarget !== resolvedWorkingDir) {
1098
+ return {
1099
+ valid: false,
1100
+ error: `Access denied: Path '${targetPath}' is outside the working directory`
1101
+ };
1102
+ }
1103
+ return { valid: true };
1104
+ }
1105
+
1094
1106
  const execAsync = util.promisify(child_process.exec);
1095
- function registerCommonHandlers(rpcHandlerManager) {
1107
+ function registerCommonHandlers(rpcHandlerManager, workingDirectory) {
1096
1108
  rpcHandlerManager.registerHandler("bash", async (data) => {
1097
1109
  logger.debug("Shell command request:", data.command);
1110
+ if (data.cwd) {
1111
+ const validation = validatePath(data.cwd, workingDirectory);
1112
+ if (!validation.valid) {
1113
+ return { success: false, error: validation.error };
1114
+ }
1115
+ }
1098
1116
  try {
1099
1117
  const options = {
1100
1118
  cwd: data.cwd,
@@ -1130,6 +1148,10 @@ function registerCommonHandlers(rpcHandlerManager) {
1130
1148
  });
1131
1149
  rpcHandlerManager.registerHandler("readFile", async (data) => {
1132
1150
  logger.debug("Read file request:", data.path);
1151
+ const validation = validatePath(data.path, workingDirectory);
1152
+ if (!validation.valid) {
1153
+ return { success: false, error: validation.error };
1154
+ }
1133
1155
  try {
1134
1156
  const buffer = await fs$2.readFile(data.path);
1135
1157
  const content = buffer.toString("base64");
@@ -1141,6 +1163,10 @@ function registerCommonHandlers(rpcHandlerManager) {
1141
1163
  });
1142
1164
  rpcHandlerManager.registerHandler("writeFile", async (data) => {
1143
1165
  logger.debug("Write file request:", data.path);
1166
+ const validation = validatePath(data.path, workingDirectory);
1167
+ if (!validation.valid) {
1168
+ return { success: false, error: validation.error };
1169
+ }
1144
1170
  try {
1145
1171
  if (data.expectedHash !== null && data.expectedHash !== void 0) {
1146
1172
  try {
@@ -1187,6 +1213,10 @@ function registerCommonHandlers(rpcHandlerManager) {
1187
1213
  });
1188
1214
  rpcHandlerManager.registerHandler("listDirectory", async (data) => {
1189
1215
  logger.debug("List directory request:", data.path);
1216
+ const validation = validatePath(data.path, workingDirectory);
1217
+ if (!validation.valid) {
1218
+ return { success: false, error: validation.error };
1219
+ }
1190
1220
  try {
1191
1221
  const entries = await fs$2.readdir(data.path, { withFileTypes: true });
1192
1222
  const directoryEntries = await Promise.all(
@@ -1228,6 +1258,10 @@ function registerCommonHandlers(rpcHandlerManager) {
1228
1258
  });
1229
1259
  rpcHandlerManager.registerHandler("getDirectoryTree", async (data) => {
1230
1260
  logger.debug("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
1261
+ const validation = validatePath(data.path, workingDirectory);
1262
+ if (!validation.valid) {
1263
+ return { success: false, error: validation.error };
1264
+ }
1231
1265
  async function buildTree(path$1, name, currentDepth) {
1232
1266
  try {
1233
1267
  const stats = await fs$2.stat(path$1);
@@ -1284,6 +1318,12 @@ function registerCommonHandlers(rpcHandlerManager) {
1284
1318
  });
1285
1319
  rpcHandlerManager.registerHandler("ripgrep", async (data) => {
1286
1320
  logger.debug("Ripgrep request with args:", data.args, "cwd:", data.cwd);
1321
+ if (data.cwd) {
1322
+ const validation = validatePath(data.cwd, workingDirectory);
1323
+ if (!validation.valid) {
1324
+ return { success: false, error: validation.error };
1325
+ }
1326
+ }
1287
1327
  try {
1288
1328
  const result = await run$1(data.args, { cwd: data.cwd });
1289
1329
  return {
@@ -1302,6 +1342,12 @@ function registerCommonHandlers(rpcHandlerManager) {
1302
1342
  });
1303
1343
  rpcHandlerManager.registerHandler("difftastic", async (data) => {
1304
1344
  logger.debug("Difftastic request with args:", data.args, "cwd:", data.cwd);
1345
+ if (data.cwd) {
1346
+ const validation = validatePath(data.cwd, workingDirectory);
1347
+ if (!validation.valid) {
1348
+ return { success: false, error: validation.error };
1349
+ }
1350
+ }
1305
1351
  try {
1306
1352
  const result = await run(data.args, { cwd: data.cwd });
1307
1353
  return {
@@ -1351,7 +1397,7 @@ class ApiSessionClient extends node_events.EventEmitter {
1351
1397
  encryptionVariant: this.encryptionVariant,
1352
1398
  logger: (msg, data) => logger.debug(msg, data)
1353
1399
  });
1354
- registerCommonHandlers(this.rpcHandlerManager);
1400
+ registerCommonHandlers(this.rpcHandlerManager, this.metadata.path);
1355
1401
  this.socket = socket_ioClient.io(configuration.serverUrl, {
1356
1402
  auth: {
1357
1403
  token: this.token,
@@ -1467,7 +1513,7 @@ class ApiSessionClient extends node_events.EventEmitter {
1467
1513
  sid: this.sessionId,
1468
1514
  message: encrypted
1469
1515
  });
1470
- if (body.type === "assistant" && body.message.usage) {
1516
+ if (body.type === "assistant" && body.message?.usage) {
1471
1517
  try {
1472
1518
  this.sendUsageData(body.message.usage);
1473
1519
  } catch (error) {
@@ -1641,7 +1687,7 @@ class ApiMachineClient {
1641
1687
  encryptionVariant: this.machine.encryptionVariant,
1642
1688
  logger: (msg, data) => logger.debug(msg, data)
1643
1689
  });
1644
- registerCommonHandlers(this.rpcHandlerManager);
1690
+ registerCommonHandlers(this.rpcHandlerManager, process.cwd());
1645
1691
  }
1646
1692
  socket;
1647
1693
  keepAliveInterval = null;
@@ -1857,7 +1903,7 @@ class PushNotificationClient {
1857
1903
  );
1858
1904
  logger.debug(`Fetched ${response.data.tokens.length} push tokens`);
1859
1905
  response.data.tokens.forEach((token, index) => {
1860
- logger.debug(`[PUSH] Token ${index + 1}: id=${token.id}, token=${token.token}, created=${new Date(token.createdAt).toISOString()}, updated=${new Date(token.updatedAt).toISOString()}`);
1906
+ logger.debug(`[PUSH] Token ${index + 1}: id=${token.id}, created=${new Date(token.createdAt).toISOString()}, updated=${new Date(token.updatedAt).toISOString()}`);
1861
1907
  });
1862
1908
  return response.data.tokens;
1863
1909
  } catch (error) {
@@ -1931,14 +1977,14 @@ class PushNotificationClient {
1931
1977
  const tokens = await this.fetchPushTokens();
1932
1978
  logger.debug(`[PUSH] Fetched ${tokens.length} push tokens`);
1933
1979
  tokens.forEach((token, index) => {
1934
- logger.debug(`[PUSH] Using token ${index + 1}: id=${token.id}, token=${token.token}`);
1980
+ logger.debug(`[PUSH] Using token ${index + 1}: id=${token.id}`);
1935
1981
  });
1936
1982
  if (tokens.length === 0) {
1937
1983
  logger.debug("No push tokens found for user");
1938
1984
  return;
1939
1985
  }
1940
1986
  const messages = tokens.map((token, index) => {
1941
- logger.debug(`[PUSH] Creating message ${index + 1} for token: ${token.token}`);
1987
+ logger.debug(`[PUSH] Creating message ${index + 1} for token`);
1942
1988
  return {
1943
1989
  to: token.token,
1944
1990
  title,
@@ -2135,17 +2181,16 @@ const RawJSONLinesSchema = z.z.discriminatedUnion("type", [
2135
2181
  // Used in sessionScanner.ts
2136
2182
  }).passthrough()
2137
2183
  }).passthrough(),
2138
- // Assistant message - validates message object with usage and content
2184
+ // Assistant message - only validates uuid and type
2185
+ // message object is optional to handle synthetic error messages (isApiErrorMessage: true)
2186
+ // which may have different structure than normal assistant messages
2139
2187
  z.z.object({
2140
2188
  uuid: z.z.string(),
2141
2189
  type: z.z.literal("assistant"),
2142
2190
  message: z.z.object({
2143
- // Entire message used in getMessageKey()
2144
- usage: UsageSchema.optional(),
2191
+ usage: UsageSchema.optional()
2145
2192
  // Used in apiSession.ts
2146
- content: z.z.any()
2147
- // Used in tests
2148
- }).passthrough()
2193
+ }).passthrough().optional()
2149
2194
  }).passthrough(),
2150
2195
  // Summary message - validates summary and leafUuid
2151
2196
  z.z.object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happy-coder",
3
- "version": "0.11.2",
3
+ "version": "0.12.0",
4
4
  "description": "Mobile and Web client for Claude Code and Codex",
5
5
  "author": "Kirill Dubovitskiy",
6
6
  "license": "MIT",
@@ -59,7 +59,9 @@
59
59
  "typecheck": "tsc --noEmit",
60
60
  "build": "shx rm -rf dist && npx tsc --noEmit && pkgroll",
61
61
  "test": "yarn build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
62
+ "test:win": "yarn build && npx vitest run",
62
63
  "start": "yarn build && ./bin/happy.mjs",
64
+ "start:win": "yarn build && node ./bin/happy.mjs",
63
65
  "dev": "tsx src/index.ts",
64
66
  "dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
65
67
  "dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
@@ -68,32 +70,30 @@
68
70
  "postinstall": "node scripts/unpack-tools.cjs"
69
71
  },
70
72
  "dependencies": {
71
- "@anthropic-ai/claude-code": "2.0.14",
72
- "@anthropic-ai/sdk": "0.65.0",
73
- "@modelcontextprotocol/sdk": "^1.15.1",
73
+ "@modelcontextprotocol/sdk": "^1.22.0",
74
74
  "@stablelib/base64": "^2.0.1",
75
75
  "@stablelib/hex": "^2.0.1",
76
76
  "@types/cross-spawn": "^6.0.6",
77
- "@types/http-proxy": "^1.17.16",
77
+ "@types/http-proxy": "^1.17.17",
78
78
  "@types/ps-list": "^6.2.1",
79
79
  "@types/qrcode-terminal": "^0.12.2",
80
- "@types/react": "^19.1.9",
80
+ "@types/react": "^19.2.7",
81
81
  "@types/tmp": "^0.2.6",
82
- "axios": "^1.10.0",
83
- "chalk": "^5.4.1",
82
+ "axios": "^1.13.2",
83
+ "chalk": "^5.6.2",
84
84
  "cross-spawn": "^7.0.6",
85
85
  "expo-server-sdk": "^3.15.0",
86
- "fastify": "^5.5.0",
86
+ "fastify": "^5.6.2",
87
87
  "fastify-type-provider-zod": "4.0.2",
88
88
  "http-proxy": "^1.18.1",
89
89
  "http-proxy-middleware": "^3.0.5",
90
- "ink": "^6.1.0",
90
+ "ink": "^6.5.1",
91
91
  "open": "^10.2.0",
92
92
  "ps-list": "^8.1.1",
93
93
  "qrcode-terminal": "^0.12.0",
94
- "react": "^19.1.1",
94
+ "react": "^19.2.0",
95
95
  "socket.io-client": "^4.8.1",
96
- "tar": "^7.4.3",
96
+ "tar": "^7.5.2",
97
97
  "tmp": "^0.2.5",
98
98
  "tweetnacl": "^1.0.3",
99
99
  "zod": "^3.23.8"
@@ -101,15 +101,15 @@
101
101
  "devDependencies": {
102
102
  "@eslint/compat": "^1",
103
103
  "@types/node": ">=20",
104
- "cross-env": "^10.0.0",
104
+ "cross-env": "^10.1.0",
105
105
  "dotenv": "^16.6.1",
106
106
  "eslint": "^9",
107
107
  "eslint-config-prettier": "^10",
108
108
  "pkgroll": "^2.14.2",
109
- "release-it": "^19.0.4",
109
+ "release-it": "^19.0.6",
110
110
  "shx": "^0.3.3",
111
111
  "ts-node": "^10",
112
- "tsx": "^4.20.3",
112
+ "tsx": "^4.20.6",
113
113
  "typescript": "^5",
114
114
  "vitest": "^3.2.4"
115
115
  },
@@ -1,4 +1,3 @@
1
- const crypto = require('crypto');
2
1
  const fs = require('fs');
3
2
 
4
3
  // Disable autoupdater (never works really)
@@ -13,34 +12,7 @@ function writeMessage(message) {
13
12
  }
14
13
  }
15
14
 
16
- // Intercept crypto.randomUUID
17
- const originalRandomUUID = crypto.randomUUID;
18
- Object.defineProperty(global, 'crypto', {
19
- configurable: true,
20
- enumerable: true,
21
- get() {
22
- return {
23
- randomUUID: () => {
24
- const uuid = originalRandomUUID();
25
- writeMessage({ type: 'uuid', value: uuid });
26
- return uuid;
27
- }
28
- };
29
- }
30
- });
31
- Object.defineProperty(crypto, 'randomUUID', {
32
- configurable: true,
33
- enumerable: true,
34
- get() {
35
- return () => {
36
- const uuid = originalRandomUUID();
37
- writeMessage({ type: 'uuid', value: uuid });
38
- return uuid;
39
- }
40
- }
41
- });
42
-
43
- // Intercept fetch to track activity
15
+ // Intercept fetch to track thinking state
44
16
  const originalFetch = global.fetch;
45
17
  let fetchCounter = 0;
46
18
 
@@ -95,4 +67,7 @@ global.fetch = function(...args) {
95
67
  Object.defineProperty(global.fetch, 'name', { value: 'fetch' });
96
68
  Object.defineProperty(global.fetch, 'length', { value: originalFetch.length });
97
69
 
98
- import('@anthropic-ai/claude-code/cli.js')
70
+ // Import global Claude Code CLI
71
+ const { getClaudeCliPath, runClaudeCli } = require('./claude_version_utils.cjs');
72
+
73
+ runClaudeCli(getClaudeCliPath());
@@ -10,4 +10,7 @@ global.setTimeout = function(callback, delay, ...args) {
10
10
  Object.defineProperty(global.setTimeout, 'name', { value: 'setTimeout' });
11
11
  Object.defineProperty(global.setTimeout, 'length', { value: originalSetTimeout.length });
12
12
 
13
- import('@anthropic-ai/claude-code/cli.js')
13
+ // Import global Claude Code CLI
14
+ const { getClaudeCliPath, runClaudeCli } = require('./claude_version_utils.cjs');
15
+
16
+ runClaudeCli(getClaudeCliPath());