happy-coder 0.6.2 → 0.6.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.
package/dist/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var chalk = require('chalk');
4
- var types$1 = require('./types-iMUxaPkI.cjs');
4
+ var types$1 = require('./types-BDtHM1DY.cjs');
5
5
  var node_crypto = require('node:crypto');
6
6
  var node_child_process = require('node:child_process');
7
7
  var node_path = require('node:path');
@@ -93,6 +93,13 @@ class Session {
93
93
  onSessionFound = (sessionId) => {
94
94
  this.sessionId = sessionId;
95
95
  };
96
+ /**
97
+ * Clear the current session ID (used by /clear command)
98
+ */
99
+ clearSessionId = () => {
100
+ this.sessionId = null;
101
+ types$1.logger.debug("[Session] Session ID cleared");
102
+ };
96
103
  }
97
104
 
98
105
  function getProjectPath(workingDirectory) {
@@ -1237,6 +1244,50 @@ class PushableAsyncIterable {
1237
1244
  }
1238
1245
  }
1239
1246
 
1247
+ function parseCompact(message) {
1248
+ const trimmed = message.trim();
1249
+ if (trimmed === "/compact") {
1250
+ return {
1251
+ isCompact: true,
1252
+ originalMessage: trimmed
1253
+ };
1254
+ }
1255
+ if (trimmed.startsWith("/compact ")) {
1256
+ return {
1257
+ isCompact: true,
1258
+ originalMessage: trimmed
1259
+ };
1260
+ }
1261
+ return {
1262
+ isCompact: false,
1263
+ originalMessage: message
1264
+ };
1265
+ }
1266
+ function parseClear(message) {
1267
+ const trimmed = message.trim();
1268
+ return {
1269
+ isClear: trimmed === "/clear"
1270
+ };
1271
+ }
1272
+ function parseSpecialCommand(message) {
1273
+ const compactResult = parseCompact(message);
1274
+ if (compactResult.isCompact) {
1275
+ return {
1276
+ type: "compact",
1277
+ originalMessage: compactResult.originalMessage
1278
+ };
1279
+ }
1280
+ const clearResult = parseClear(message);
1281
+ if (clearResult.isClear) {
1282
+ return {
1283
+ type: "clear"
1284
+ };
1285
+ }
1286
+ return {
1287
+ type: null
1288
+ };
1289
+ }
1290
+
1240
1291
  async function claudeRemote(opts) {
1241
1292
  let startFrom = opts.sessionId;
1242
1293
  if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
@@ -1270,6 +1321,21 @@ async function claudeRemote(opts) {
1270
1321
  sdkOptions.executableArgs = [...sdkOptions.executableArgs || [], ...opts.claudeArgs];
1271
1322
  }
1272
1323
  types$1.logger.debug(`[claudeRemote] Starting query with permission mode: ${opts.permissionMode}, model: ${opts.model || "default"}, fallbackModel: ${opts.fallbackModel || "none"}, customSystemPrompt: ${opts.customSystemPrompt ? "set" : "none"}, appendSystemPrompt: ${opts.appendSystemPrompt ? "set" : "none"}, allowedTools: ${opts.allowedTools ? opts.allowedTools.join(",") : "none"}, disallowedTools: ${opts.disallowedTools ? opts.disallowedTools.join(",") : "none"}`);
1324
+ const specialCommand = parseSpecialCommand(opts.message);
1325
+ if (specialCommand.type === "clear") {
1326
+ types$1.logger.debug("[claudeRemote] /clear command detected - should not reach here, handled in start.ts");
1327
+ if (opts.onCompletionEvent) {
1328
+ opts.onCompletionEvent("Context was reset");
1329
+ }
1330
+ if (opts.onSessionReset) {
1331
+ opts.onSessionReset();
1332
+ }
1333
+ return;
1334
+ }
1335
+ if (specialCommand.type === "compact") {
1336
+ types$1.logger.debug("[claudeRemote] /compact command detected - will process as normal but with compaction behavior");
1337
+ }
1338
+ const isCompactCommand = specialCommand.type === "compact";
1273
1339
  let message = new PushableAsyncIterable();
1274
1340
  message.push({
1275
1341
  type: "user",
@@ -1309,10 +1375,22 @@ async function claudeRemote(opts) {
1309
1375
  types$1.logger.debug(`[claudeRemote] Session file found: ${systemInit.session_id} ${found}`);
1310
1376
  opts.onSessionFound(systemInit.session_id);
1311
1377
  }
1378
+ if (isCompactCommand) {
1379
+ types$1.logger.debug("[claudeRemote] Compaction started");
1380
+ if (opts.onCompletionEvent) {
1381
+ opts.onCompletionEvent("Compaction started");
1382
+ }
1383
+ }
1312
1384
  }
1313
1385
  if (message2.type === "result") {
1314
1386
  updateThinking(false);
1315
1387
  types$1.logger.debug("[claudeRemote] Result received, exiting claudeRemote");
1388
+ if (isCompactCommand) {
1389
+ types$1.logger.debug("[claudeRemote] Compaction completed");
1390
+ if (opts.onCompletionEvent) {
1391
+ opts.onCompletionEvent("Compaction completed");
1392
+ }
1393
+ }
1316
1394
  return;
1317
1395
  }
1318
1396
  if (message2.type === "user") {
@@ -2177,6 +2255,14 @@ async function claudeRemoteLauncher(session) {
2177
2255
  claudeEnvVars: session.claudeEnvVars,
2178
2256
  claudeArgs: session.claudeArgs,
2179
2257
  onMessage,
2258
+ onCompletionEvent: (message) => {
2259
+ types$1.logger.debug(`[remote]: Completion event: ${message}`);
2260
+ session.client.sendSessionEvent({ type: "message", message });
2261
+ },
2262
+ onSessionReset: () => {
2263
+ types$1.logger.debug("[remote]: Session reset");
2264
+ session.clearSessionId();
2265
+ },
2180
2266
  signal: abortController.signal
2181
2267
  });
2182
2268
  if (!exitReason && abortController.signal.aborted) {
@@ -2233,6 +2319,9 @@ async function loop(opts) {
2233
2319
  messageQueue: opts.messageQueue,
2234
2320
  onModeChange: opts.onModeChange
2235
2321
  });
2322
+ if (opts.onSessionReady) {
2323
+ opts.onSessionReady(session);
2324
+ }
2236
2325
  let mode = opts.startingMode ?? "local";
2237
2326
  while (true) {
2238
2327
  types$1.logger.debug(`[loop] Iteration with mode: ${mode}`);
@@ -2262,7 +2351,7 @@ async function loop(opts) {
2262
2351
  }
2263
2352
 
2264
2353
  var name = "happy-coder";
2265
- var version = "0.6.2";
2354
+ var version = "0.6.4";
2266
2355
  var description = "Claude Code session sharing CLI";
2267
2356
  var author = "Kirill Dubovitskiy";
2268
2357
  var license = "MIT";
@@ -2669,6 +2758,7 @@ class MessageQueue2 {
2669
2758
  types$1.logger.debug(`[MessageQueue2] Initialized`);
2670
2759
  }
2671
2760
  queue = [];
2761
+ // Made public for testing
2672
2762
  waiter = null;
2673
2763
  closed = false;
2674
2764
  onMessageHandler = null;
@@ -2690,7 +2780,8 @@ class MessageQueue2 {
2690
2780
  this.queue.push({
2691
2781
  message,
2692
2782
  mode,
2693
- modeHash
2783
+ modeHash,
2784
+ isolate: false
2694
2785
  });
2695
2786
  if (this.onMessageHandler) {
2696
2787
  this.onMessageHandler(message, mode);
@@ -2703,6 +2794,62 @@ class MessageQueue2 {
2703
2794
  }
2704
2795
  types$1.logger.debug(`[MessageQueue2] push() completed. Queue size: ${this.queue.length}`);
2705
2796
  }
2797
+ /**
2798
+ * Push a message immediately without batching delay.
2799
+ * Does not clear the queue or enforce isolation.
2800
+ */
2801
+ pushImmediate(message, mode) {
2802
+ if (this.closed) {
2803
+ throw new Error("Cannot push to closed queue");
2804
+ }
2805
+ const modeHash = this.modeHasher(mode);
2806
+ types$1.logger.debug(`[MessageQueue2] pushImmediate() called with mode hash: ${modeHash}`);
2807
+ this.queue.push({
2808
+ message,
2809
+ mode,
2810
+ modeHash,
2811
+ isolate: false
2812
+ });
2813
+ if (this.onMessageHandler) {
2814
+ this.onMessageHandler(message, mode);
2815
+ }
2816
+ if (this.waiter) {
2817
+ types$1.logger.debug(`[MessageQueue2] Notifying waiter for immediate message`);
2818
+ const waiter = this.waiter;
2819
+ this.waiter = null;
2820
+ waiter(true);
2821
+ }
2822
+ types$1.logger.debug(`[MessageQueue2] pushImmediate() completed. Queue size: ${this.queue.length}`);
2823
+ }
2824
+ /**
2825
+ * Push a message that must be processed in complete isolation.
2826
+ * Clears any pending messages and ensures this message is never batched with others.
2827
+ * Used for special commands that require dedicated processing.
2828
+ */
2829
+ pushIsolateAndClear(message, mode) {
2830
+ if (this.closed) {
2831
+ throw new Error("Cannot push to closed queue");
2832
+ }
2833
+ const modeHash = this.modeHasher(mode);
2834
+ types$1.logger.debug(`[MessageQueue2] pushIsolateAndClear() called with mode hash: ${modeHash} - clearing ${this.queue.length} pending messages`);
2835
+ this.queue = [];
2836
+ this.queue.push({
2837
+ message,
2838
+ mode,
2839
+ modeHash,
2840
+ isolate: true
2841
+ });
2842
+ if (this.onMessageHandler) {
2843
+ this.onMessageHandler(message, mode);
2844
+ }
2845
+ if (this.waiter) {
2846
+ types$1.logger.debug(`[MessageQueue2] Notifying waiter for isolated message`);
2847
+ const waiter = this.waiter;
2848
+ this.waiter = null;
2849
+ waiter(true);
2850
+ }
2851
+ types$1.logger.debug(`[MessageQueue2] pushIsolateAndClear() completed. Queue size: ${this.queue.length}`);
2852
+ }
2706
2853
  /**
2707
2854
  * Push a message to the beginning of the queue with a mode.
2708
2855
  */
@@ -2715,7 +2862,8 @@ class MessageQueue2 {
2715
2862
  this.queue.unshift({
2716
2863
  message,
2717
2864
  mode,
2718
- modeHash
2865
+ modeHash,
2866
+ isolate: false
2719
2867
  });
2720
2868
  if (this.onMessageHandler) {
2721
2869
  this.onMessageHandler(message, mode);
@@ -2779,7 +2927,7 @@ class MessageQueue2 {
2779
2927
  return this.collectBatch();
2780
2928
  }
2781
2929
  /**
2782
- * Collect a batch of messages with the same mode
2930
+ * Collect a batch of messages with the same mode, respecting isolation requirements
2783
2931
  */
2784
2932
  collectBatch() {
2785
2933
  if (this.queue.length === 0) {
@@ -2789,12 +2937,18 @@ class MessageQueue2 {
2789
2937
  const sameModeMessages = [];
2790
2938
  let mode = firstItem.mode;
2791
2939
  const targetModeHash = firstItem.modeHash;
2792
- while (this.queue.length > 0 && this.queue[0].modeHash === targetModeHash) {
2940
+ if (firstItem.isolate) {
2793
2941
  const item = this.queue.shift();
2794
2942
  sameModeMessages.push(item.message);
2943
+ types$1.logger.debug(`[MessageQueue2] Collected isolated message with mode hash: ${targetModeHash}`);
2944
+ } else {
2945
+ while (this.queue.length > 0 && this.queue[0].modeHash === targetModeHash && !this.queue[0].isolate) {
2946
+ const item = this.queue.shift();
2947
+ sameModeMessages.push(item.message);
2948
+ }
2949
+ types$1.logger.debug(`[MessageQueue2] Collected batch of ${sameModeMessages.length} messages with mode hash: ${targetModeHash}`);
2795
2950
  }
2796
2951
  const combinedMessage = sameModeMessages.join("\n");
2797
- types$1.logger.debug(`[MessageQueue2] Collected batch of ${sameModeMessages.length} messages with mode hash: ${targetModeHash}`);
2798
2952
  return {
2799
2953
  message: combinedMessage,
2800
2954
  mode
@@ -3089,7 +3243,7 @@ async function start(credentials, options = {}) {
3089
3243
  types$1.logger.infoDeveloper(`Logs: ${logPath}`);
3090
3244
  session.updateAgentState((currentState) => ({
3091
3245
  ...currentState,
3092
- controlledByUser: options.startingMode === "local"
3246
+ controlledByUser: options.startingMode !== "remote"
3093
3247
  }));
3094
3248
  const caffeinateStarted = startCaffeinate();
3095
3249
  if (caffeinateStarted) {
@@ -3166,6 +3320,37 @@ async function start(credentials, options = {}) {
3166
3320
  } else {
3167
3321
  types$1.logger.debug(`[loop] User message received with no disallowed tools override, using current: ${currentDisallowedTools ? currentDisallowedTools.join(", ") : "none"}`);
3168
3322
  }
3323
+ const specialCommand = parseSpecialCommand(message.content.text);
3324
+ if (specialCommand.type === "compact") {
3325
+ types$1.logger.debug("[start] Detected /compact command");
3326
+ const enhancedMode2 = {
3327
+ permissionMode: messagePermissionMode || "default",
3328
+ model: messageModel,
3329
+ fallbackModel: messageFallbackModel,
3330
+ customSystemPrompt: messageCustomSystemPrompt,
3331
+ appendSystemPrompt: messageAppendSystemPrompt,
3332
+ allowedTools: messageAllowedTools,
3333
+ disallowedTools: messageDisallowedTools
3334
+ };
3335
+ messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode2);
3336
+ types$1.logger.debugLargeJson("[start] /compact command pushed to queue:", message);
3337
+ return;
3338
+ }
3339
+ if (specialCommand.type === "clear") {
3340
+ types$1.logger.debug("[start] Detected /clear command");
3341
+ const enhancedMode2 = {
3342
+ permissionMode: messagePermissionMode || "default",
3343
+ model: messageModel,
3344
+ fallbackModel: messageFallbackModel,
3345
+ customSystemPrompt: messageCustomSystemPrompt,
3346
+ appendSystemPrompt: messageAppendSystemPrompt,
3347
+ allowedTools: messageAllowedTools,
3348
+ disallowedTools: messageDisallowedTools
3349
+ };
3350
+ messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode2);
3351
+ types$1.logger.debugLargeJson("[start] /compact command pushed to queue:", message);
3352
+ return;
3353
+ }
3169
3354
  const enhancedMode = {
3170
3355
  permissionMode: messagePermissionMode || "default",
3171
3356
  model: messageModel,
@@ -3192,6 +3377,8 @@ async function start(credentials, options = {}) {
3192
3377
  controlledByUser: newMode === "local"
3193
3378
  }));
3194
3379
  },
3380
+ onSessionReady: (sessionInstance) => {
3381
+ },
3195
3382
  mcpServers: {},
3196
3383
  session,
3197
3384
  claudeEnvVars: options.claudeEnvVars,
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { l as logger, d as backoff, e as delay, R as RawJSONLinesSchema, c as configuration, f as encodeBase64, A as ApiClient, g as encodeBase64Url, h as decodeBase64, j as encrypt, b as initializeConfiguration, i as initLoggerWithGlobalConfiguration } from './types-DKVMGtcN.mjs';
2
+ import { l as logger, d as backoff, e as delay, R as RawJSONLinesSchema, c as configuration, f as encodeBase64, A as ApiClient, g as encodeBase64Url, h as decodeBase64, j as encrypt, b as initializeConfiguration, i as initLoggerWithGlobalConfiguration } from './types-Dz5kZrVh.mjs';
3
3
  import { randomUUID, randomBytes } from 'node:crypto';
4
4
  import { spawn, execSync } from 'node:child_process';
5
5
  import { resolve, join, dirname as dirname$1 } from 'node:path';
@@ -72,6 +72,13 @@ class Session {
72
72
  onSessionFound = (sessionId) => {
73
73
  this.sessionId = sessionId;
74
74
  };
75
+ /**
76
+ * Clear the current session ID (used by /clear command)
77
+ */
78
+ clearSessionId = () => {
79
+ this.sessionId = null;
80
+ logger.debug("[Session] Session ID cleared");
81
+ };
75
82
  }
76
83
 
77
84
  function getProjectPath(workingDirectory) {
@@ -1216,6 +1223,50 @@ class PushableAsyncIterable {
1216
1223
  }
1217
1224
  }
1218
1225
 
1226
+ function parseCompact(message) {
1227
+ const trimmed = message.trim();
1228
+ if (trimmed === "/compact") {
1229
+ return {
1230
+ isCompact: true,
1231
+ originalMessage: trimmed
1232
+ };
1233
+ }
1234
+ if (trimmed.startsWith("/compact ")) {
1235
+ return {
1236
+ isCompact: true,
1237
+ originalMessage: trimmed
1238
+ };
1239
+ }
1240
+ return {
1241
+ isCompact: false,
1242
+ originalMessage: message
1243
+ };
1244
+ }
1245
+ function parseClear(message) {
1246
+ const trimmed = message.trim();
1247
+ return {
1248
+ isClear: trimmed === "/clear"
1249
+ };
1250
+ }
1251
+ function parseSpecialCommand(message) {
1252
+ const compactResult = parseCompact(message);
1253
+ if (compactResult.isCompact) {
1254
+ return {
1255
+ type: "compact",
1256
+ originalMessage: compactResult.originalMessage
1257
+ };
1258
+ }
1259
+ const clearResult = parseClear(message);
1260
+ if (clearResult.isClear) {
1261
+ return {
1262
+ type: "clear"
1263
+ };
1264
+ }
1265
+ return {
1266
+ type: null
1267
+ };
1268
+ }
1269
+
1219
1270
  async function claudeRemote(opts) {
1220
1271
  let startFrom = opts.sessionId;
1221
1272
  if (opts.sessionId && !claudeCheckSession(opts.sessionId, opts.path)) {
@@ -1249,6 +1300,21 @@ async function claudeRemote(opts) {
1249
1300
  sdkOptions.executableArgs = [...sdkOptions.executableArgs || [], ...opts.claudeArgs];
1250
1301
  }
1251
1302
  logger.debug(`[claudeRemote] Starting query with permission mode: ${opts.permissionMode}, model: ${opts.model || "default"}, fallbackModel: ${opts.fallbackModel || "none"}, customSystemPrompt: ${opts.customSystemPrompt ? "set" : "none"}, appendSystemPrompt: ${opts.appendSystemPrompt ? "set" : "none"}, allowedTools: ${opts.allowedTools ? opts.allowedTools.join(",") : "none"}, disallowedTools: ${opts.disallowedTools ? opts.disallowedTools.join(",") : "none"}`);
1303
+ const specialCommand = parseSpecialCommand(opts.message);
1304
+ if (specialCommand.type === "clear") {
1305
+ logger.debug("[claudeRemote] /clear command detected - should not reach here, handled in start.ts");
1306
+ if (opts.onCompletionEvent) {
1307
+ opts.onCompletionEvent("Context was reset");
1308
+ }
1309
+ if (opts.onSessionReset) {
1310
+ opts.onSessionReset();
1311
+ }
1312
+ return;
1313
+ }
1314
+ if (specialCommand.type === "compact") {
1315
+ logger.debug("[claudeRemote] /compact command detected - will process as normal but with compaction behavior");
1316
+ }
1317
+ const isCompactCommand = specialCommand.type === "compact";
1252
1318
  let message = new PushableAsyncIterable();
1253
1319
  message.push({
1254
1320
  type: "user",
@@ -1288,10 +1354,22 @@ async function claudeRemote(opts) {
1288
1354
  logger.debug(`[claudeRemote] Session file found: ${systemInit.session_id} ${found}`);
1289
1355
  opts.onSessionFound(systemInit.session_id);
1290
1356
  }
1357
+ if (isCompactCommand) {
1358
+ logger.debug("[claudeRemote] Compaction started");
1359
+ if (opts.onCompletionEvent) {
1360
+ opts.onCompletionEvent("Compaction started");
1361
+ }
1362
+ }
1291
1363
  }
1292
1364
  if (message2.type === "result") {
1293
1365
  updateThinking(false);
1294
1366
  logger.debug("[claudeRemote] Result received, exiting claudeRemote");
1367
+ if (isCompactCommand) {
1368
+ logger.debug("[claudeRemote] Compaction completed");
1369
+ if (opts.onCompletionEvent) {
1370
+ opts.onCompletionEvent("Compaction completed");
1371
+ }
1372
+ }
1295
1373
  return;
1296
1374
  }
1297
1375
  if (message2.type === "user") {
@@ -2156,6 +2234,14 @@ async function claudeRemoteLauncher(session) {
2156
2234
  claudeEnvVars: session.claudeEnvVars,
2157
2235
  claudeArgs: session.claudeArgs,
2158
2236
  onMessage,
2237
+ onCompletionEvent: (message) => {
2238
+ logger.debug(`[remote]: Completion event: ${message}`);
2239
+ session.client.sendSessionEvent({ type: "message", message });
2240
+ },
2241
+ onSessionReset: () => {
2242
+ logger.debug("[remote]: Session reset");
2243
+ session.clearSessionId();
2244
+ },
2159
2245
  signal: abortController.signal
2160
2246
  });
2161
2247
  if (!exitReason && abortController.signal.aborted) {
@@ -2212,6 +2298,9 @@ async function loop(opts) {
2212
2298
  messageQueue: opts.messageQueue,
2213
2299
  onModeChange: opts.onModeChange
2214
2300
  });
2301
+ if (opts.onSessionReady) {
2302
+ opts.onSessionReady(session);
2303
+ }
2215
2304
  let mode = opts.startingMode ?? "local";
2216
2305
  while (true) {
2217
2306
  logger.debug(`[loop] Iteration with mode: ${mode}`);
@@ -2241,7 +2330,7 @@ async function loop(opts) {
2241
2330
  }
2242
2331
 
2243
2332
  var name = "happy-coder";
2244
- var version = "0.6.2";
2333
+ var version = "0.6.4";
2245
2334
  var description = "Claude Code session sharing CLI";
2246
2335
  var author = "Kirill Dubovitskiy";
2247
2336
  var license = "MIT";
@@ -2648,6 +2737,7 @@ class MessageQueue2 {
2648
2737
  logger.debug(`[MessageQueue2] Initialized`);
2649
2738
  }
2650
2739
  queue = [];
2740
+ // Made public for testing
2651
2741
  waiter = null;
2652
2742
  closed = false;
2653
2743
  onMessageHandler = null;
@@ -2669,7 +2759,8 @@ class MessageQueue2 {
2669
2759
  this.queue.push({
2670
2760
  message,
2671
2761
  mode,
2672
- modeHash
2762
+ modeHash,
2763
+ isolate: false
2673
2764
  });
2674
2765
  if (this.onMessageHandler) {
2675
2766
  this.onMessageHandler(message, mode);
@@ -2682,6 +2773,62 @@ class MessageQueue2 {
2682
2773
  }
2683
2774
  logger.debug(`[MessageQueue2] push() completed. Queue size: ${this.queue.length}`);
2684
2775
  }
2776
+ /**
2777
+ * Push a message immediately without batching delay.
2778
+ * Does not clear the queue or enforce isolation.
2779
+ */
2780
+ pushImmediate(message, mode) {
2781
+ if (this.closed) {
2782
+ throw new Error("Cannot push to closed queue");
2783
+ }
2784
+ const modeHash = this.modeHasher(mode);
2785
+ logger.debug(`[MessageQueue2] pushImmediate() called with mode hash: ${modeHash}`);
2786
+ this.queue.push({
2787
+ message,
2788
+ mode,
2789
+ modeHash,
2790
+ isolate: false
2791
+ });
2792
+ if (this.onMessageHandler) {
2793
+ this.onMessageHandler(message, mode);
2794
+ }
2795
+ if (this.waiter) {
2796
+ logger.debug(`[MessageQueue2] Notifying waiter for immediate message`);
2797
+ const waiter = this.waiter;
2798
+ this.waiter = null;
2799
+ waiter(true);
2800
+ }
2801
+ logger.debug(`[MessageQueue2] pushImmediate() completed. Queue size: ${this.queue.length}`);
2802
+ }
2803
+ /**
2804
+ * Push a message that must be processed in complete isolation.
2805
+ * Clears any pending messages and ensures this message is never batched with others.
2806
+ * Used for special commands that require dedicated processing.
2807
+ */
2808
+ pushIsolateAndClear(message, mode) {
2809
+ if (this.closed) {
2810
+ throw new Error("Cannot push to closed queue");
2811
+ }
2812
+ const modeHash = this.modeHasher(mode);
2813
+ logger.debug(`[MessageQueue2] pushIsolateAndClear() called with mode hash: ${modeHash} - clearing ${this.queue.length} pending messages`);
2814
+ this.queue = [];
2815
+ this.queue.push({
2816
+ message,
2817
+ mode,
2818
+ modeHash,
2819
+ isolate: true
2820
+ });
2821
+ if (this.onMessageHandler) {
2822
+ this.onMessageHandler(message, mode);
2823
+ }
2824
+ if (this.waiter) {
2825
+ logger.debug(`[MessageQueue2] Notifying waiter for isolated message`);
2826
+ const waiter = this.waiter;
2827
+ this.waiter = null;
2828
+ waiter(true);
2829
+ }
2830
+ logger.debug(`[MessageQueue2] pushIsolateAndClear() completed. Queue size: ${this.queue.length}`);
2831
+ }
2685
2832
  /**
2686
2833
  * Push a message to the beginning of the queue with a mode.
2687
2834
  */
@@ -2694,7 +2841,8 @@ class MessageQueue2 {
2694
2841
  this.queue.unshift({
2695
2842
  message,
2696
2843
  mode,
2697
- modeHash
2844
+ modeHash,
2845
+ isolate: false
2698
2846
  });
2699
2847
  if (this.onMessageHandler) {
2700
2848
  this.onMessageHandler(message, mode);
@@ -2758,7 +2906,7 @@ class MessageQueue2 {
2758
2906
  return this.collectBatch();
2759
2907
  }
2760
2908
  /**
2761
- * Collect a batch of messages with the same mode
2909
+ * Collect a batch of messages with the same mode, respecting isolation requirements
2762
2910
  */
2763
2911
  collectBatch() {
2764
2912
  if (this.queue.length === 0) {
@@ -2768,12 +2916,18 @@ class MessageQueue2 {
2768
2916
  const sameModeMessages = [];
2769
2917
  let mode = firstItem.mode;
2770
2918
  const targetModeHash = firstItem.modeHash;
2771
- while (this.queue.length > 0 && this.queue[0].modeHash === targetModeHash) {
2919
+ if (firstItem.isolate) {
2772
2920
  const item = this.queue.shift();
2773
2921
  sameModeMessages.push(item.message);
2922
+ logger.debug(`[MessageQueue2] Collected isolated message with mode hash: ${targetModeHash}`);
2923
+ } else {
2924
+ while (this.queue.length > 0 && this.queue[0].modeHash === targetModeHash && !this.queue[0].isolate) {
2925
+ const item = this.queue.shift();
2926
+ sameModeMessages.push(item.message);
2927
+ }
2928
+ logger.debug(`[MessageQueue2] Collected batch of ${sameModeMessages.length} messages with mode hash: ${targetModeHash}`);
2774
2929
  }
2775
2930
  const combinedMessage = sameModeMessages.join("\n");
2776
- logger.debug(`[MessageQueue2] Collected batch of ${sameModeMessages.length} messages with mode hash: ${targetModeHash}`);
2777
2931
  return {
2778
2932
  message: combinedMessage,
2779
2933
  mode
@@ -3068,7 +3222,7 @@ async function start(credentials, options = {}) {
3068
3222
  logger.infoDeveloper(`Logs: ${logPath}`);
3069
3223
  session.updateAgentState((currentState) => ({
3070
3224
  ...currentState,
3071
- controlledByUser: options.startingMode === "local"
3225
+ controlledByUser: options.startingMode !== "remote"
3072
3226
  }));
3073
3227
  const caffeinateStarted = startCaffeinate();
3074
3228
  if (caffeinateStarted) {
@@ -3145,6 +3299,37 @@ async function start(credentials, options = {}) {
3145
3299
  } else {
3146
3300
  logger.debug(`[loop] User message received with no disallowed tools override, using current: ${currentDisallowedTools ? currentDisallowedTools.join(", ") : "none"}`);
3147
3301
  }
3302
+ const specialCommand = parseSpecialCommand(message.content.text);
3303
+ if (specialCommand.type === "compact") {
3304
+ logger.debug("[start] Detected /compact command");
3305
+ const enhancedMode2 = {
3306
+ permissionMode: messagePermissionMode || "default",
3307
+ model: messageModel,
3308
+ fallbackModel: messageFallbackModel,
3309
+ customSystemPrompt: messageCustomSystemPrompt,
3310
+ appendSystemPrompt: messageAppendSystemPrompt,
3311
+ allowedTools: messageAllowedTools,
3312
+ disallowedTools: messageDisallowedTools
3313
+ };
3314
+ messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode2);
3315
+ logger.debugLargeJson("[start] /compact command pushed to queue:", message);
3316
+ return;
3317
+ }
3318
+ if (specialCommand.type === "clear") {
3319
+ logger.debug("[start] Detected /clear command");
3320
+ const enhancedMode2 = {
3321
+ permissionMode: messagePermissionMode || "default",
3322
+ model: messageModel,
3323
+ fallbackModel: messageFallbackModel,
3324
+ customSystemPrompt: messageCustomSystemPrompt,
3325
+ appendSystemPrompt: messageAppendSystemPrompt,
3326
+ allowedTools: messageAllowedTools,
3327
+ disallowedTools: messageDisallowedTools
3328
+ };
3329
+ messageQueue.pushIsolateAndClear(specialCommand.originalMessage || message.content.text, enhancedMode2);
3330
+ logger.debugLargeJson("[start] /compact command pushed to queue:", message);
3331
+ return;
3332
+ }
3148
3333
  const enhancedMode = {
3149
3334
  permissionMode: messagePermissionMode || "default",
3150
3335
  model: messageModel,
@@ -3171,6 +3356,8 @@ async function start(credentials, options = {}) {
3171
3356
  controlledByUser: newMode === "local"
3172
3357
  }));
3173
3358
  },
3359
+ onSessionReady: (sessionInstance) => {
3360
+ },
3174
3361
  mcpServers: {},
3175
3362
  session,
3176
3363
  claudeEnvVars: options.claudeEnvVars,
package/dist/lib.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var types = require('./types-iMUxaPkI.cjs');
3
+ var types = require('./types-BDtHM1DY.cjs');
4
4
  require('axios');
5
5
  require('chalk');
6
6
  require('fs');
package/dist/lib.d.cts CHANGED
@@ -449,6 +449,8 @@ declare class ApiSessionClient extends EventEmitter {
449
449
  private pendingMessages;
450
450
  private pendingMessageCallback;
451
451
  private rpcHandlers;
452
+ private agentStateLock;
453
+ private metadataLock;
452
454
  constructor(token: string, secret: Uint8Array, session: Session);
453
455
  onUserMessage(callback: (data: UserMessage) => void): void;
454
456
  /**
package/dist/lib.d.mts CHANGED
@@ -449,6 +449,8 @@ declare class ApiSessionClient extends EventEmitter {
449
449
  private pendingMessages;
450
450
  private pendingMessageCallback;
451
451
  private rpcHandlers;
452
+ private agentStateLock;
453
+ private metadataLock;
452
454
  constructor(token: string, secret: Uint8Array, session: Session);
453
455
  onUserMessage(callback: (data: UserMessage) => void): void;
454
456
  /**
package/dist/lib.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export { A as ApiClient, a as ApiSessionClient, R as RawJSONLinesSchema, c as configuration, i as initLoggerWithGlobalConfiguration, b as initializeConfiguration, l as logger } from './types-DKVMGtcN.mjs';
1
+ export { A as ApiClient, a as ApiSessionClient, R as RawJSONLinesSchema, c as configuration, i as initLoggerWithGlobalConfiguration, b as initializeConfiguration, l as logger } from './types-Dz5kZrVh.mjs';
2
2
  import 'axios';
3
3
  import 'chalk';
4
4
  import 'fs';
@@ -358,6 +358,40 @@ function createBackoff(opts) {
358
358
  }
359
359
  let backoff = createBackoff();
360
360
 
361
+ class AsyncLock {
362
+ permits = 1;
363
+ promiseResolverQueue = [];
364
+ async inLock(func) {
365
+ try {
366
+ await this.lock();
367
+ return await func();
368
+ } finally {
369
+ this.unlock();
370
+ }
371
+ }
372
+ async lock() {
373
+ if (this.permits > 0) {
374
+ this.permits = this.permits - 1;
375
+ return;
376
+ }
377
+ await new Promise((resolve) => this.promiseResolverQueue.push(resolve));
378
+ }
379
+ unlock() {
380
+ this.permits += 1;
381
+ if (this.permits > 1 && this.promiseResolverQueue.length > 0) {
382
+ throw new Error("this.permits should never be > 0 when there is someone waiting.");
383
+ } else if (this.permits === 1 && this.promiseResolverQueue.length > 0) {
384
+ this.permits -= 1;
385
+ const nextResolver = this.promiseResolverQueue.shift();
386
+ if (nextResolver) {
387
+ setTimeout(() => {
388
+ nextResolver(true);
389
+ }, 0);
390
+ }
391
+ }
392
+ }
393
+ }
394
+
361
395
  class ApiSessionClient extends node_events.EventEmitter {
362
396
  token;
363
397
  secret;
@@ -370,6 +404,8 @@ class ApiSessionClient extends node_events.EventEmitter {
370
404
  pendingMessages = [];
371
405
  pendingMessageCallback = null;
372
406
  rpcHandlers = /* @__PURE__ */ new Map();
407
+ agentStateLock = new AsyncLock();
408
+ metadataLock = new AsyncLock();
373
409
  constructor(token, secret, session) {
374
410
  super();
375
411
  this.token = token;
@@ -589,19 +625,21 @@ class ApiSessionClient extends node_events.EventEmitter {
589
625
  * @param handler - Handler function that returns the updated metadata
590
626
  */
591
627
  updateMetadata(handler) {
592
- backoff(async () => {
593
- let updated = handler(this.metadata);
594
- const answer = await this.socket.emitWithAck("update-metadata", { sid: this.sessionId, expectedVersion: this.metadataVersion, metadata: encodeBase64(encrypt(updated, this.secret)) });
595
- if (answer.result === "success") {
596
- this.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
597
- this.metadataVersion = answer.version;
598
- } else if (answer.result === "version-mismatch") {
599
- if (answer.version > this.metadataVersion) {
600
- this.metadataVersion = answer.version;
628
+ this.metadataLock.inLock(async () => {
629
+ await backoff(async () => {
630
+ let updated = handler(this.metadata);
631
+ const answer = await this.socket.emitWithAck("update-metadata", { sid: this.sessionId, expectedVersion: this.metadataVersion, metadata: encodeBase64(encrypt(updated, this.secret)) });
632
+ if (answer.result === "success") {
601
633
  this.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
602
- }
603
- throw new Error("Metadata version mismatch");
604
- } else if (answer.result === "error") ;
634
+ this.metadataVersion = answer.version;
635
+ } else if (answer.result === "version-mismatch") {
636
+ if (answer.version > this.metadataVersion) {
637
+ this.metadataVersion = answer.version;
638
+ this.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
639
+ }
640
+ throw new Error("Metadata version mismatch");
641
+ } else if (answer.result === "error") ;
642
+ });
605
643
  });
606
644
  }
607
645
  /**
@@ -610,20 +648,22 @@ class ApiSessionClient extends node_events.EventEmitter {
610
648
  */
611
649
  updateAgentState(handler) {
612
650
  exports.logger.debugLargeJson("Updating agent state", this.agentState);
613
- backoff(async () => {
614
- let updated = handler(this.agentState || {});
615
- const answer = await this.socket.emitWithAck("update-state", { sid: this.sessionId, expectedVersion: this.agentStateVersion, agentState: updated ? encodeBase64(encrypt(updated, this.secret)) : null });
616
- if (answer.result === "success") {
617
- this.agentState = answer.agentState ? decrypt(decodeBase64(answer.agentState), this.secret) : null;
618
- this.agentStateVersion = answer.version;
619
- exports.logger.debug("Agent state updated", this.agentState);
620
- } else if (answer.result === "version-mismatch") {
621
- if (answer.version > this.agentStateVersion) {
622
- this.agentStateVersion = answer.version;
651
+ this.agentStateLock.inLock(async () => {
652
+ await backoff(async () => {
653
+ let updated = handler(this.agentState || {});
654
+ const answer = await this.socket.emitWithAck("update-state", { sid: this.sessionId, expectedVersion: this.agentStateVersion, agentState: updated ? encodeBase64(encrypt(updated, this.secret)) : null });
655
+ if (answer.result === "success") {
623
656
  this.agentState = answer.agentState ? decrypt(decodeBase64(answer.agentState), this.secret) : null;
624
- }
625
- throw new Error("Agent state version mismatch");
626
- } else if (answer.result === "error") ;
657
+ this.agentStateVersion = answer.version;
658
+ exports.logger.debug("Agent state updated", this.agentState);
659
+ } else if (answer.result === "version-mismatch") {
660
+ if (answer.version > this.agentStateVersion) {
661
+ this.agentStateVersion = answer.version;
662
+ this.agentState = answer.agentState ? decrypt(decodeBase64(answer.agentState), this.secret) : null;
663
+ }
664
+ throw new Error("Agent state version mismatch");
665
+ } else if (answer.result === "error") ;
666
+ });
627
667
  });
628
668
  }
629
669
  /**
@@ -356,6 +356,40 @@ function createBackoff(opts) {
356
356
  }
357
357
  let backoff = createBackoff();
358
358
 
359
+ class AsyncLock {
360
+ permits = 1;
361
+ promiseResolverQueue = [];
362
+ async inLock(func) {
363
+ try {
364
+ await this.lock();
365
+ return await func();
366
+ } finally {
367
+ this.unlock();
368
+ }
369
+ }
370
+ async lock() {
371
+ if (this.permits > 0) {
372
+ this.permits = this.permits - 1;
373
+ return;
374
+ }
375
+ await new Promise((resolve) => this.promiseResolverQueue.push(resolve));
376
+ }
377
+ unlock() {
378
+ this.permits += 1;
379
+ if (this.permits > 1 && this.promiseResolverQueue.length > 0) {
380
+ throw new Error("this.permits should never be > 0 when there is someone waiting.");
381
+ } else if (this.permits === 1 && this.promiseResolverQueue.length > 0) {
382
+ this.permits -= 1;
383
+ const nextResolver = this.promiseResolverQueue.shift();
384
+ if (nextResolver) {
385
+ setTimeout(() => {
386
+ nextResolver(true);
387
+ }, 0);
388
+ }
389
+ }
390
+ }
391
+ }
392
+
359
393
  class ApiSessionClient extends EventEmitter {
360
394
  token;
361
395
  secret;
@@ -368,6 +402,8 @@ class ApiSessionClient extends EventEmitter {
368
402
  pendingMessages = [];
369
403
  pendingMessageCallback = null;
370
404
  rpcHandlers = /* @__PURE__ */ new Map();
405
+ agentStateLock = new AsyncLock();
406
+ metadataLock = new AsyncLock();
371
407
  constructor(token, secret, session) {
372
408
  super();
373
409
  this.token = token;
@@ -587,19 +623,21 @@ class ApiSessionClient extends EventEmitter {
587
623
  * @param handler - Handler function that returns the updated metadata
588
624
  */
589
625
  updateMetadata(handler) {
590
- backoff(async () => {
591
- let updated = handler(this.metadata);
592
- const answer = await this.socket.emitWithAck("update-metadata", { sid: this.sessionId, expectedVersion: this.metadataVersion, metadata: encodeBase64(encrypt(updated, this.secret)) });
593
- if (answer.result === "success") {
594
- this.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
595
- this.metadataVersion = answer.version;
596
- } else if (answer.result === "version-mismatch") {
597
- if (answer.version > this.metadataVersion) {
598
- this.metadataVersion = answer.version;
626
+ this.metadataLock.inLock(async () => {
627
+ await backoff(async () => {
628
+ let updated = handler(this.metadata);
629
+ const answer = await this.socket.emitWithAck("update-metadata", { sid: this.sessionId, expectedVersion: this.metadataVersion, metadata: encodeBase64(encrypt(updated, this.secret)) });
630
+ if (answer.result === "success") {
599
631
  this.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
600
- }
601
- throw new Error("Metadata version mismatch");
602
- } else if (answer.result === "error") ;
632
+ this.metadataVersion = answer.version;
633
+ } else if (answer.result === "version-mismatch") {
634
+ if (answer.version > this.metadataVersion) {
635
+ this.metadataVersion = answer.version;
636
+ this.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
637
+ }
638
+ throw new Error("Metadata version mismatch");
639
+ } else if (answer.result === "error") ;
640
+ });
603
641
  });
604
642
  }
605
643
  /**
@@ -608,20 +646,22 @@ class ApiSessionClient extends EventEmitter {
608
646
  */
609
647
  updateAgentState(handler) {
610
648
  logger.debugLargeJson("Updating agent state", this.agentState);
611
- backoff(async () => {
612
- let updated = handler(this.agentState || {});
613
- const answer = await this.socket.emitWithAck("update-state", { sid: this.sessionId, expectedVersion: this.agentStateVersion, agentState: updated ? encodeBase64(encrypt(updated, this.secret)) : null });
614
- if (answer.result === "success") {
615
- this.agentState = answer.agentState ? decrypt(decodeBase64(answer.agentState), this.secret) : null;
616
- this.agentStateVersion = answer.version;
617
- logger.debug("Agent state updated", this.agentState);
618
- } else if (answer.result === "version-mismatch") {
619
- if (answer.version > this.agentStateVersion) {
620
- this.agentStateVersion = answer.version;
649
+ this.agentStateLock.inLock(async () => {
650
+ await backoff(async () => {
651
+ let updated = handler(this.agentState || {});
652
+ const answer = await this.socket.emitWithAck("update-state", { sid: this.sessionId, expectedVersion: this.agentStateVersion, agentState: updated ? encodeBase64(encrypt(updated, this.secret)) : null });
653
+ if (answer.result === "success") {
621
654
  this.agentState = answer.agentState ? decrypt(decodeBase64(answer.agentState), this.secret) : null;
622
- }
623
- throw new Error("Agent state version mismatch");
624
- } else if (answer.result === "error") ;
655
+ this.agentStateVersion = answer.version;
656
+ logger.debug("Agent state updated", this.agentState);
657
+ } else if (answer.result === "version-mismatch") {
658
+ if (answer.version > this.agentStateVersion) {
659
+ this.agentStateVersion = answer.version;
660
+ this.agentState = answer.agentState ? decrypt(decodeBase64(answer.agentState), this.secret) : null;
661
+ }
662
+ throw new Error("Agent state version mismatch");
663
+ } else if (answer.result === "error") ;
664
+ });
625
665
  });
626
666
  }
627
667
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happy-coder",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Claude Code session sharing CLI",
5
5
  "author": "Kirill Dubovitskiy",
6
6
  "license": "MIT",