happy-coder 0.11.2-0 → 0.12.0-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-0";
24
+ var version = "0.12.0-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.13",
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({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happy-coder",
3
- "version": "0.11.2-0",
3
+ "version": "0.12.0-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.13",
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());