happy-coder 0.9.0-6 → 0.9.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.
@@ -3,27 +3,174 @@
3
3
  var axios = require('axios');
4
4
  var chalk = require('chalk');
5
5
  var fs = require('fs');
6
+ var node_fs = require('node:fs');
6
7
  var os = require('node:os');
7
8
  var node_path = require('node:path');
8
9
  var promises = require('node:fs/promises');
9
- var node_fs = require('node:fs');
10
- var node_events = require('node:events');
11
- var socket_ioClient = require('socket.io-client');
12
10
  var z = require('zod');
13
11
  var node_crypto = require('node:crypto');
14
12
  var tweetnacl = require('tweetnacl');
13
+ var node_events = require('node:events');
14
+ var socket_ioClient = require('socket.io-client');
15
15
  var expoServerSdk = require('expo-server-sdk');
16
16
 
17
+ function _interopNamespaceDefault(e) {
18
+ var n = Object.create(null);
19
+ if (e) {
20
+ Object.keys(e).forEach(function (k) {
21
+ if (k !== 'default') {
22
+ var d = Object.getOwnPropertyDescriptor(e, k);
23
+ Object.defineProperty(n, k, d.get ? d : {
24
+ enumerable: true,
25
+ get: function () { return e[k]; }
26
+ });
27
+ }
28
+ });
29
+ }
30
+ n.default = e;
31
+ return Object.freeze(n);
32
+ }
33
+
34
+ var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
35
+
36
+ var name = "happy-coder";
37
+ var version = "0.9.0";
38
+ var description = "Claude Code session sharing CLI";
39
+ var author = "Kirill Dubovitskiy";
40
+ var license = "MIT";
41
+ var type = "module";
42
+ var homepage = "https://github.com/slopus/happy-cli";
43
+ var bugs = "https://github.com/slopus/happy-cli/issues";
44
+ var repository = "slopus/happy-cli";
45
+ var bin = {
46
+ happy: "./bin/happy.mjs"
47
+ };
48
+ var main = "./dist/index.cjs";
49
+ var module$1 = "./dist/index.mjs";
50
+ var types = "./dist/index.d.cts";
51
+ var exports$1 = {
52
+ ".": {
53
+ require: {
54
+ types: "./dist/index.d.cts",
55
+ "default": "./dist/index.cjs"
56
+ },
57
+ "import": {
58
+ types: "./dist/index.d.mts",
59
+ "default": "./dist/index.mjs"
60
+ }
61
+ },
62
+ "./lib": {
63
+ require: {
64
+ types: "./dist/lib.d.cts",
65
+ "default": "./dist/lib.cjs"
66
+ },
67
+ "import": {
68
+ types: "./dist/lib.d.mts",
69
+ "default": "./dist/lib.mjs"
70
+ }
71
+ }
72
+ };
73
+ var files = [
74
+ "dist",
75
+ "bin",
76
+ "scripts",
77
+ "ripgrep",
78
+ "package.json"
79
+ ];
80
+ var scripts = {
81
+ "why do we need to build before running tests / dev?": "We need the binary to be built so we run daemon commands which directly run the binary - we don't want them to go out of sync or have custom spawn logic depending how we started happy",
82
+ typecheck: "tsc --noEmit",
83
+ build: "shx rm -rf dist && npx tsc --noEmit && pkgroll",
84
+ test: "yarn build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
85
+ start: "yarn build && ./bin/happy.mjs",
86
+ dev: "yarn build && tsx --env-file .env.dev src/index.ts",
87
+ "dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
88
+ "dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
89
+ prepublishOnly: "yarn build && yarn test",
90
+ release: "release-it"
91
+ };
92
+ var dependencies = {
93
+ "@anthropic-ai/claude-code": "^1.0.89",
94
+ "@anthropic-ai/sdk": "^0.56.0",
95
+ "@modelcontextprotocol/sdk": "^1.15.1",
96
+ "@stablelib/base64": "^2.0.1",
97
+ "@types/http-proxy": "^1.17.16",
98
+ "@types/qrcode-terminal": "^0.12.2",
99
+ "@types/react": "^19.1.9",
100
+ axios: "^1.10.0",
101
+ chalk: "^5.4.1",
102
+ "expo-server-sdk": "^3.15.0",
103
+ fastify: "^5.5.0",
104
+ "fastify-type-provider-zod": "4.0.2",
105
+ "http-proxy": "^1.18.1",
106
+ "http-proxy-middleware": "^3.0.5",
107
+ ink: "^6.1.0",
108
+ open: "^10.2.0",
109
+ "qrcode-terminal": "^0.12.0",
110
+ react: "^19.1.1",
111
+ "socket.io-client": "^4.8.1",
112
+ tweetnacl: "^1.0.3",
113
+ zod: "^3.23.8"
114
+ };
115
+ var devDependencies = {
116
+ "@eslint/compat": "^1",
117
+ "@types/node": ">=20",
118
+ "cross-env": "^10.0.0",
119
+ dotenv: "^16.6.1",
120
+ eslint: "^9",
121
+ "eslint-config-prettier": "^10",
122
+ pkgroll: "^2.14.2",
123
+ "release-it": "^19.0.4",
124
+ shx: "^0.3.3",
125
+ "ts-node": "^10",
126
+ tsx: "^4.20.3",
127
+ typescript: "^5",
128
+ vitest: "^3.2.4"
129
+ };
130
+ var resolutions = {
131
+ "whatwg-url": "14.2.0",
132
+ "parse-path": "7.0.3",
133
+ "@types/parse-path": "7.0.3"
134
+ };
135
+ var publishConfig = {
136
+ registry: "https://registry.npmjs.org"
137
+ };
138
+ var packageManager = "yarn@1.22.22";
139
+ var packageJson = {
140
+ name: name,
141
+ version: version,
142
+ description: description,
143
+ author: author,
144
+ license: license,
145
+ type: type,
146
+ homepage: homepage,
147
+ bugs: bugs,
148
+ repository: repository,
149
+ bin: bin,
150
+ main: main,
151
+ module: module$1,
152
+ types: types,
153
+ exports: exports$1,
154
+ files: files,
155
+ scripts: scripts,
156
+ dependencies: dependencies,
157
+ devDependencies: devDependencies,
158
+ resolutions: resolutions,
159
+ publishConfig: publishConfig,
160
+ packageManager: packageManager
161
+ };
162
+
17
163
  class Configuration {
18
164
  serverUrl;
19
165
  isDaemonProcess;
20
166
  // Directories and paths (from persistence)
21
167
  happyHomeDir;
22
168
  logsDir;
23
- daemonLogsDir;
24
169
  settingsFile;
25
170
  privateKeyFile;
26
171
  daemonStateFile;
172
+ daemonLockFile;
173
+ currentCliVersion;
27
174
  isExperimentalEnabled;
28
175
  constructor() {
29
176
  this.serverUrl = process.env.HAPPY_SERVER_URL || "https://handy-api.korshakov.org";
@@ -36,15 +183,231 @@ class Configuration {
36
183
  this.happyHomeDir = node_path.join(os.homedir(), ".happy");
37
184
  }
38
185
  this.logsDir = node_path.join(this.happyHomeDir, "logs");
39
- this.daemonLogsDir = node_path.join(this.happyHomeDir, "logs-daemon");
40
186
  this.settingsFile = node_path.join(this.happyHomeDir, "settings.json");
41
187
  this.privateKeyFile = node_path.join(this.happyHomeDir, "access.key");
42
188
  this.daemonStateFile = node_path.join(this.happyHomeDir, "daemon.state.json");
189
+ this.daemonLockFile = node_path.join(this.happyHomeDir, "daemon.state.json.lock");
43
190
  this.isExperimentalEnabled = ["true", "1", "yes"].includes(process.env.HAPPY_EXPERIMENTAL?.toLowerCase() || "");
191
+ this.currentCliVersion = packageJson.version;
192
+ if (!node_fs.existsSync(this.happyHomeDir)) {
193
+ node_fs.mkdirSync(this.happyHomeDir, { recursive: true });
194
+ }
195
+ if (!node_fs.existsSync(this.logsDir)) {
196
+ node_fs.mkdirSync(this.logsDir, { recursive: true });
197
+ }
44
198
  }
45
199
  }
46
200
  const configuration = new Configuration();
47
201
 
202
+ function encodeBase64(buffer, variant = "base64") {
203
+ if (variant === "base64url") {
204
+ return encodeBase64Url(buffer);
205
+ }
206
+ return Buffer.from(buffer).toString("base64");
207
+ }
208
+ function encodeBase64Url(buffer) {
209
+ return Buffer.from(buffer).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
210
+ }
211
+ function decodeBase64(base64, variant = "base64") {
212
+ if (variant === "base64url") {
213
+ const base64Standard = base64.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - base64.length % 4) % 4);
214
+ return new Uint8Array(Buffer.from(base64Standard, "base64"));
215
+ }
216
+ return new Uint8Array(Buffer.from(base64, "base64"));
217
+ }
218
+ function getRandomBytes(size) {
219
+ return new Uint8Array(node_crypto.randomBytes(size));
220
+ }
221
+ function encrypt(data, secret) {
222
+ const nonce = getRandomBytes(tweetnacl.secretbox.nonceLength);
223
+ const encrypted = tweetnacl.secretbox(new TextEncoder().encode(JSON.stringify(data)), nonce, secret);
224
+ const result = new Uint8Array(nonce.length + encrypted.length);
225
+ result.set(nonce);
226
+ result.set(encrypted, nonce.length);
227
+ return result;
228
+ }
229
+ function decrypt(data, secret) {
230
+ const nonce = data.slice(0, tweetnacl.secretbox.nonceLength);
231
+ const encrypted = data.slice(tweetnacl.secretbox.nonceLength);
232
+ const decrypted = tweetnacl.secretbox.open(encrypted, nonce, secret);
233
+ if (!decrypted) {
234
+ return null;
235
+ }
236
+ return JSON.parse(new TextDecoder().decode(decrypted));
237
+ }
238
+
239
+ const defaultSettings = {
240
+ onboardingCompleted: false
241
+ };
242
+ async function readSettings() {
243
+ if (!node_fs.existsSync(configuration.settingsFile)) {
244
+ return { ...defaultSettings };
245
+ }
246
+ try {
247
+ const content = await promises.readFile(configuration.settingsFile, "utf8");
248
+ return JSON.parse(content);
249
+ } catch {
250
+ return { ...defaultSettings };
251
+ }
252
+ }
253
+ async function updateSettings(updater) {
254
+ const LOCK_RETRY_INTERVAL_MS = 100;
255
+ const MAX_LOCK_ATTEMPTS = 50;
256
+ const STALE_LOCK_TIMEOUT_MS = 1e4;
257
+ const lockFile = configuration.settingsFile + ".lock";
258
+ const tmpFile = configuration.settingsFile + ".tmp";
259
+ let fileHandle;
260
+ let attempts = 0;
261
+ while (attempts < MAX_LOCK_ATTEMPTS) {
262
+ try {
263
+ fileHandle = await promises.open(lockFile, node_fs.constants.O_CREAT | node_fs.constants.O_EXCL | node_fs.constants.O_WRONLY);
264
+ break;
265
+ } catch (err) {
266
+ if (err.code === "EEXIST") {
267
+ attempts++;
268
+ await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS));
269
+ try {
270
+ const stats = await promises.stat(lockFile);
271
+ if (Date.now() - stats.mtimeMs > STALE_LOCK_TIMEOUT_MS) {
272
+ await promises.unlink(lockFile).catch(() => {
273
+ });
274
+ }
275
+ } catch {
276
+ }
277
+ } else {
278
+ throw err;
279
+ }
280
+ }
281
+ }
282
+ if (!fileHandle) {
283
+ throw new Error(`Failed to acquire settings lock after ${MAX_LOCK_ATTEMPTS * LOCK_RETRY_INTERVAL_MS / 1e3} seconds`);
284
+ }
285
+ try {
286
+ const current = await readSettings() || { ...defaultSettings };
287
+ const updated = await updater(current);
288
+ if (!node_fs.existsSync(configuration.happyHomeDir)) {
289
+ await promises.mkdir(configuration.happyHomeDir, { recursive: true });
290
+ }
291
+ await promises.writeFile(tmpFile, JSON.stringify(updated, null, 2));
292
+ await promises.rename(tmpFile, configuration.settingsFile);
293
+ return updated;
294
+ } finally {
295
+ await fileHandle.close();
296
+ await promises.unlink(lockFile).catch(() => {
297
+ });
298
+ }
299
+ }
300
+ const credentialsSchema = z__namespace.object({
301
+ secret: z__namespace.string().base64(),
302
+ token: z__namespace.string()
303
+ });
304
+ async function readCredentials() {
305
+ if (!node_fs.existsSync(configuration.privateKeyFile)) {
306
+ return null;
307
+ }
308
+ try {
309
+ const keyBase64 = await promises.readFile(configuration.privateKeyFile, "utf8");
310
+ const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
311
+ return {
312
+ secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
313
+ token: credentials.token
314
+ };
315
+ } catch {
316
+ return null;
317
+ }
318
+ }
319
+ async function writeCredentials(credentials) {
320
+ if (!node_fs.existsSync(configuration.happyHomeDir)) {
321
+ await promises.mkdir(configuration.happyHomeDir, { recursive: true });
322
+ }
323
+ await promises.writeFile(configuration.privateKeyFile, JSON.stringify({
324
+ secret: encodeBase64(credentials.secret),
325
+ token: credentials.token
326
+ }, null, 2));
327
+ }
328
+ async function clearCredentials() {
329
+ if (node_fs.existsSync(configuration.privateKeyFile)) {
330
+ await promises.unlink(configuration.privateKeyFile);
331
+ }
332
+ }
333
+ async function clearMachineId() {
334
+ await updateSettings((settings) => ({
335
+ ...settings,
336
+ machineId: void 0
337
+ }));
338
+ }
339
+ async function readDaemonState() {
340
+ try {
341
+ if (!node_fs.existsSync(configuration.daemonStateFile)) {
342
+ return null;
343
+ }
344
+ const content = await promises.readFile(configuration.daemonStateFile, "utf-8");
345
+ return JSON.parse(content);
346
+ } catch (error) {
347
+ console.error(`[PERSISTENCE] Daemon state file corrupted: ${configuration.daemonStateFile}`, error);
348
+ return null;
349
+ }
350
+ }
351
+ function writeDaemonState(state) {
352
+ node_fs.writeFileSync(configuration.daemonStateFile, JSON.stringify(state, null, 2), "utf-8");
353
+ }
354
+ async function clearDaemonState() {
355
+ if (node_fs.existsSync(configuration.daemonStateFile)) {
356
+ await promises.unlink(configuration.daemonStateFile);
357
+ }
358
+ if (node_fs.existsSync(configuration.daemonLockFile)) {
359
+ try {
360
+ await promises.unlink(configuration.daemonLockFile);
361
+ } catch {
362
+ }
363
+ }
364
+ }
365
+ async function acquireDaemonLock(maxAttempts = 5, delayIncrementMs = 200) {
366
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
367
+ try {
368
+ const fileHandle = await promises.open(
369
+ configuration.daemonLockFile,
370
+ node_fs.constants.O_CREAT | node_fs.constants.O_EXCL | node_fs.constants.O_WRONLY
371
+ );
372
+ await fileHandle.writeFile(String(process.pid));
373
+ return fileHandle;
374
+ } catch (error) {
375
+ if (error.code === "EEXIST") {
376
+ try {
377
+ const lockPid = node_fs.readFileSync(configuration.daemonLockFile, "utf-8").trim();
378
+ if (lockPid && !isNaN(Number(lockPid))) {
379
+ try {
380
+ process.kill(Number(lockPid), 0);
381
+ } catch {
382
+ node_fs.unlinkSync(configuration.daemonLockFile);
383
+ continue;
384
+ }
385
+ }
386
+ } catch {
387
+ }
388
+ }
389
+ if (attempt === maxAttempts) {
390
+ return null;
391
+ }
392
+ const delayMs = attempt * delayIncrementMs;
393
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
394
+ }
395
+ }
396
+ return null;
397
+ }
398
+ async function releaseDaemonLock(lockHandle) {
399
+ try {
400
+ await lockHandle.close();
401
+ } catch {
402
+ }
403
+ try {
404
+ if (node_fs.existsSync(configuration.daemonLockFile)) {
405
+ node_fs.unlinkSync(configuration.daemonLockFile);
406
+ }
407
+ } catch {
408
+ }
409
+ }
410
+
48
411
  function createTimestampForFilename(date = /* @__PURE__ */ new Date()) {
49
412
  return date.toLocaleString("sv-SE", {
50
413
  timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
@@ -54,7 +417,7 @@ function createTimestampForFilename(date = /* @__PURE__ */ new Date()) {
54
417
  hour: "2-digit",
55
418
  minute: "2-digit",
56
419
  second: "2-digit"
57
- }).replace(/[: ]/g, "-").replace(/,/g, "");
420
+ }).replace(/[: ]/g, "-").replace(/,/g, "") + "-pid-" + process.pid;
58
421
  }
59
422
  function createTimestampForLogEntry(date = /* @__PURE__ */ new Date()) {
60
423
  return date.toLocaleTimeString("en-US", {
@@ -66,17 +429,14 @@ function createTimestampForLogEntry(date = /* @__PURE__ */ new Date()) {
66
429
  fractionalSecondDigits: 3
67
430
  });
68
431
  }
69
- async function getSessionLogPath() {
70
- if (!node_fs.existsSync(configuration.logsDir)) {
71
- await promises.mkdir(configuration.logsDir, { recursive: true });
72
- }
432
+ function getSessionLogPath() {
73
433
  const timestamp = createTimestampForFilename();
74
434
  const filename = configuration.isDaemonProcess ? `${timestamp}-daemon.log` : `${timestamp}.log`;
75
435
  return node_path.join(configuration.logsDir, filename);
76
436
  }
77
437
  class Logger {
78
- constructor(logFilePathPromise = getSessionLogPath()) {
79
- this.logFilePathPromise = logFilePathPromise;
438
+ constructor(logFilePath = getSessionLogPath()) {
439
+ this.logFilePath = logFilePath;
80
440
  if (process.env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING && process.env.HAPPY_SERVER_URL) {
81
441
  this.dangerouslyUnencryptedServerLoggingUrl = process.env.HAPPY_SERVER_URL;
82
442
  console.log(chalk.yellow("[REMOTE LOGGING] Sending logs to server for AI debugging"));
@@ -189,25 +549,59 @@ class Logger {
189
549
  this.sendToRemoteServer(level, message, ...args).catch(() => {
190
550
  });
191
551
  }
192
- this.logFilePathPromise.then((logFilePath) => {
193
- try {
194
- fs.appendFileSync(logFilePath, logLine);
195
- } catch (appendError) {
196
- if (process.env.DEBUG) {
197
- console.error("Failed to append to log file:", appendError);
198
- throw appendError;
199
- }
200
- }
201
- }).catch((error) => {
552
+ try {
553
+ fs.appendFileSync(this.logFilePath, logLine);
554
+ } catch (appendError) {
202
555
  if (process.env.DEBUG) {
203
- console.log("This message only visible in DEBUG mode, not in production");
204
- console.error("Failed to resolve log file path:", error);
205
- console.log(prefix, message, ...args);
556
+ console.error("Failed to append to log file:", appendError);
557
+ throw appendError;
206
558
  }
207
- });
559
+ }
208
560
  }
209
561
  }
210
562
  let logger = new Logger();
563
+ async function listDaemonLogFiles(limit = 50) {
564
+ try {
565
+ const logsDir = configuration.logsDir;
566
+ if (!node_fs.existsSync(logsDir)) {
567
+ return [];
568
+ }
569
+ const logs = node_fs.readdirSync(logsDir).filter((file) => file.endsWith("-daemon.log")).map((file) => {
570
+ const fullPath = node_path.join(logsDir, file);
571
+ const stats = node_fs.statSync(fullPath);
572
+ return { file, path: fullPath, modified: stats.mtime };
573
+ }).sort((a, b) => b.modified.getTime() - a.modified.getTime());
574
+ try {
575
+ const state = await readDaemonState();
576
+ if (!state) {
577
+ return logs;
578
+ }
579
+ if (state.daemonLogPath && node_fs.existsSync(state.daemonLogPath)) {
580
+ const stats = node_fs.statSync(state.daemonLogPath);
581
+ const persisted = {
582
+ file: node_path.basename(state.daemonLogPath),
583
+ path: state.daemonLogPath,
584
+ modified: stats.mtime
585
+ };
586
+ const idx = logs.findIndex((l) => l.path === persisted.path);
587
+ if (idx >= 0) {
588
+ const [found] = logs.splice(idx, 1);
589
+ logs.unshift(found);
590
+ } else {
591
+ logs.unshift(persisted);
592
+ }
593
+ }
594
+ } catch {
595
+ }
596
+ return logs.slice(0, Math.max(0, limit));
597
+ } catch {
598
+ return [];
599
+ }
600
+ }
601
+ async function getLatestDaemonLog() {
602
+ const [latest] = await listDaemonLogFiles(1);
603
+ return latest || null;
604
+ }
211
605
 
212
606
  const SessionMessageContentSchema = z.z.object({
213
607
  c: z.z.string(),
@@ -364,44 +758,6 @@ const AgentMessageSchema = z.z.object({
364
758
  });
365
759
  z.z.union([UserMessageSchema, AgentMessageSchema]);
366
760
 
367
- function encodeBase64(buffer, variant = "base64") {
368
- if (variant === "base64url") {
369
- return encodeBase64Url(buffer);
370
- }
371
- return Buffer.from(buffer).toString("base64");
372
- }
373
- function encodeBase64Url(buffer) {
374
- return Buffer.from(buffer).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
375
- }
376
- function decodeBase64(base64, variant = "base64") {
377
- if (variant === "base64url") {
378
- const base64Standard = base64.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - base64.length % 4) % 4);
379
- return new Uint8Array(Buffer.from(base64Standard, "base64"));
380
- }
381
- return new Uint8Array(Buffer.from(base64, "base64"));
382
- }
383
- function getRandomBytes(size) {
384
- return new Uint8Array(node_crypto.randomBytes(size));
385
- }
386
- function encrypt(data, secret) {
387
- const nonce = getRandomBytes(tweetnacl.secretbox.nonceLength);
388
- const encrypted = tweetnacl.secretbox(new TextEncoder().encode(JSON.stringify(data)), nonce, secret);
389
- const result = new Uint8Array(nonce.length + encrypted.length);
390
- result.set(nonce);
391
- result.set(encrypted, nonce.length);
392
- return result;
393
- }
394
- function decrypt(data, secret) {
395
- const nonce = data.slice(0, tweetnacl.secretbox.nonceLength);
396
- const encrypted = data.slice(tweetnacl.secretbox.nonceLength);
397
- const decrypted = tweetnacl.secretbox.open(encrypted, nonce, secret);
398
- if (!decrypted) {
399
- logger.debug("[ERROR] Decryption failed");
400
- return null;
401
- }
402
- return JSON.parse(new TextDecoder().decode(decrypted));
403
- }
404
-
405
761
  async function delay(ms) {
406
762
  return new Promise((resolve) => setTimeout(resolve, ms));
407
763
  }
@@ -655,6 +1011,7 @@ class ApiSessionClient extends node_events.EventEmitter {
655
1011
  * Send a ping message to keep the connection alive
656
1012
  */
657
1013
  keepAlive(thinking, mode) {
1014
+ logger.debug(`[API] Sending keep alive message: ${thinking}`);
658
1015
  this.socket.volatile.emit("session-alive", {
659
1016
  sid: this.sessionId,
660
1017
  time: Date.now(),
@@ -907,11 +1264,17 @@ class ApiMachineClient {
907
1264
  if (!session) {
908
1265
  throw new Error("Failed to spawn session");
909
1266
  }
910
- logger.debug(`[API MACHINE] Spawned session ${session.happySessionId || "pending"} with PID ${session.pid}`);
1267
+ if (session.error) {
1268
+ throw new Error(session.error);
1269
+ }
1270
+ logger.debug(`[API MACHINE] Spawned session ${session.happySessionId || "WARNING - not session Id recieved in webhook"} with PID ${session.pid}`);
911
1271
  if (!session.happySessionId) {
912
1272
  throw new Error(`Session spawned (PID ${session.pid}) but no sessionId received from webhook. The session process may still be initializing.`);
913
1273
  }
914
- const response = { sessionId: session.happySessionId };
1274
+ const response = {
1275
+ sessionId: session.happySessionId,
1276
+ message: session.message
1277
+ };
915
1278
  logger.debug(`[API MACHINE] Sending RPC response:`, response);
916
1279
  callback(encodeBase64(encrypt(response, this.secret)));
917
1280
  return;
@@ -997,9 +1360,7 @@ class ApiMachineClient {
997
1360
  machineId: this.machine.id,
998
1361
  time: Date.now()
999
1362
  };
1000
- if (process.env.VERBOSE) {
1001
- logger.debugLargeJson(`[API MACHINE] Emitting machine-alive`, payload);
1002
- }
1363
+ logger.debugLargeJson(`[API MACHINE] Emitting machine-alive`, payload);
1003
1364
  this.socket.emit("machine-alive", payload);
1004
1365
  }, 2e4);
1005
1366
  logger.debug("[API MACHINE] Keep-alive started (20s interval)");
@@ -1245,6 +1606,11 @@ class ApiClient {
1245
1606
  timeout: 5e3
1246
1607
  }
1247
1608
  );
1609
+ if (response.status !== 200) {
1610
+ console.error(chalk.red(`[API] Failed to create machine: ${response.statusText}`));
1611
+ console.log(chalk.yellow(`[API] Failed to create machine: ${response.statusText}, most likely you have re-authenticated, but you still have a machine associated with the old account. Now we are trying to re-associate the machine with the new account. That is not allowed. Please run 'happy doctor clean' to clean up your happy state, and try your original command again. Please create an issue on github if this is causing you problems. We apologize for the inconvenience.`));
1612
+ process.exit(1);
1613
+ }
1248
1614
  const raw = response.data.machine;
1249
1615
  logger.debug(`[API] Machine ${opts.machineId} registered/updated with server`);
1250
1616
  const machine = {
@@ -1323,10 +1689,23 @@ exports.ApiClient = ApiClient;
1323
1689
  exports.ApiSessionClient = ApiSessionClient;
1324
1690
  exports.AsyncLock = AsyncLock;
1325
1691
  exports.RawJSONLinesSchema = RawJSONLinesSchema;
1692
+ exports.acquireDaemonLock = acquireDaemonLock;
1326
1693
  exports.backoff = backoff;
1694
+ exports.clearCredentials = clearCredentials;
1695
+ exports.clearDaemonState = clearDaemonState;
1696
+ exports.clearMachineId = clearMachineId;
1327
1697
  exports.configuration = configuration;
1328
1698
  exports.decodeBase64 = decodeBase64;
1329
1699
  exports.delay = delay;
1330
1700
  exports.encodeBase64 = encodeBase64;
1331
1701
  exports.encodeBase64Url = encodeBase64Url;
1702
+ exports.getLatestDaemonLog = getLatestDaemonLog;
1332
1703
  exports.logger = logger;
1704
+ exports.packageJson = packageJson;
1705
+ exports.readCredentials = readCredentials;
1706
+ exports.readDaemonState = readDaemonState;
1707
+ exports.readSettings = readSettings;
1708
+ exports.releaseDaemonLock = releaseDaemonLock;
1709
+ exports.updateSettings = updateSettings;
1710
+ exports.writeCredentials = writeCredentials;
1711
+ exports.writeDaemonState = writeDaemonState;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happy-coder",
3
- "version": "0.9.0-6",
3
+ "version": "0.9.0",
4
4
  "description": "Claude Code session sharing CLI",
5
5
  "author": "Kirill Dubovitskiy",
6
6
  "license": "MIT",
@@ -47,10 +47,9 @@
47
47
  "why do we need to build before running tests / dev?": "We need the binary to be built so we run daemon commands which directly run the binary - we don't want them to go out of sync or have custom spawn logic depending how we started happy",
48
48
  "typecheck": "tsc --noEmit",
49
49
  "build": "shx rm -rf dist && npx tsc --noEmit && pkgroll",
50
- "test": "yarn build && vitest run",
51
- "test:watch": "vitest",
52
- "test:integration-test-env": "yarn build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
53
- "dev": "yarn build && DEBUG=1 npx tsx src/index.ts",
50
+ "test": "yarn build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
51
+ "start": "yarn build && ./bin/happy.mjs",
52
+ "dev": "yarn build && tsx --env-file .env.dev src/index.ts",
54
53
  "dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
55
54
  "dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
56
55
  "prepublishOnly": "yarn build && yarn test",
@@ -83,6 +82,7 @@
83
82
  "@eslint/compat": "^1",
84
83
  "@types/node": ">=20",
85
84
  "cross-env": "^10.0.0",
85
+ "dotenv": "^16.6.1",
86
86
  "eslint": "^9",
87
87
  "eslint-config-prettier": "^10",
88
88
  "pkgroll": "^2.14.2",
@@ -102,4 +102,4 @@
102
102
  "registry": "https://registry.npmjs.org"
103
103
  },
104
104
  "packageManager": "yarn@1.22.22"
105
- }
105
+ }