happy-coder 0.7.2 → 0.8.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.
package/dist/index.cjs CHANGED
@@ -63,6 +63,7 @@ class Session {
63
63
  claudeEnvVars;
64
64
  claudeArgs;
65
65
  mcpServers;
66
+ allowedTools;
66
67
  _onModeChange;
67
68
  sessionId;
68
69
  mode = "local";
@@ -77,6 +78,7 @@ class Session {
77
78
  this.claudeEnvVars = opts.claudeEnvVars;
78
79
  this.claudeArgs = opts.claudeArgs;
79
80
  this.mcpServers = opts.mcpServers;
81
+ this.allowedTools = opts.allowedTools;
80
82
  this._onModeChange = opts.onModeChange;
81
83
  this.client.keepAlive(this.thinking, this.mode);
82
84
  setInterval(() => {
@@ -135,6 +137,29 @@ function projectPath() {
135
137
  return path$1;
136
138
  }
137
139
 
140
+ function trimIdent(text) {
141
+ const lines = text.split("\n");
142
+ while (lines.length > 0 && lines[0].trim() === "") {
143
+ lines.shift();
144
+ }
145
+ while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
146
+ lines.pop();
147
+ }
148
+ const minSpaces = lines.reduce((min, line) => {
149
+ if (line.trim() === "") {
150
+ return min;
151
+ }
152
+ const leadingSpaces = line.match(/^\s*/)[0].length;
153
+ return Math.min(min, leadingSpaces);
154
+ }, Infinity);
155
+ const trimmedLines = lines.map((line) => line.slice(minSpaces));
156
+ return trimmedLines.join("\n");
157
+ }
158
+
159
+ const systemPrompt = trimIdent(`
160
+ ALWAYS when you start a new chat - you must call a tool "mcp__happy__change_title" to set a chat title. When you think chat title is not relevant anymore - call the tool again to change it. When chat name is too generic and you have a change to make it more specific - call the tool again to change it. This title is needed to easily find the chat in the future. Help human.
161
+ `);
162
+
138
163
  node_path.dirname(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
139
164
  async function claudeLocal(opts) {
140
165
  const projectDir = getProjectPath(opts.path);
@@ -182,6 +207,13 @@ async function claudeLocal(opts) {
182
207
  if (startFrom) {
183
208
  args.push("--resume", startFrom);
184
209
  }
210
+ args.push("--append-system-prompt", systemPrompt);
211
+ if (opts.mcpServers && Object.keys(opts.mcpServers).length > 0) {
212
+ args.push("--mcp-config", JSON.stringify({ mcpServers: opts.mcpServers }));
213
+ }
214
+ if (opts.allowedTools && opts.allowedTools.length > 0) {
215
+ args.push("--allowedTools", opts.allowedTools.join(","));
216
+ }
185
217
  if (opts.claudeArgs) {
186
218
  args.push(...opts.claudeArgs);
187
219
  }
@@ -526,7 +558,9 @@ async function claudeLocalLauncher(session) {
526
558
  sessionId: session.sessionId,
527
559
  workingDirectory: session.path,
528
560
  onMessage: (message) => {
529
- session.client.sendClaudeSessionMessage(message);
561
+ if (message.type !== "summary") {
562
+ session.client.sendClaudeSessionMessage(message);
563
+ }
530
564
  }
531
565
  });
532
566
  let exitReason = null;
@@ -556,7 +590,9 @@ async function claudeLocalLauncher(session) {
556
590
  }
557
591
  session.client.setHandler("abort", doAbort);
558
592
  session.client.setHandler("switch", doSwitch);
559
- session.queue.setOnMessage(doSwitch);
593
+ session.queue.setOnMessage((message, mode) => {
594
+ doSwitch();
595
+ });
560
596
  if (session.queue.size() > 0) {
561
597
  return "switch";
562
598
  }
@@ -577,7 +613,9 @@ async function claudeLocalLauncher(session) {
577
613
  onThinkingChange: session.onThinkingChange,
578
614
  abort: processAbortController.signal,
579
615
  claudeEnvVars: session.claudeEnvVars,
580
- claudeArgs: session.claudeArgs
616
+ claudeArgs: session.claudeArgs,
617
+ mcpServers: session.mcpServers,
618
+ allowedTools: session.allowedTools
581
619
  });
582
620
  if (!exitReason) {
583
621
  exitReason = "exit";
@@ -1339,6 +1377,12 @@ async function claudeRemote(opts) {
1339
1377
  types$1.logger.debug("[claudeRemote] /compact command detected - will process as normal but with compaction behavior");
1340
1378
  }
1341
1379
  const isCompactCommand = specialCommand.type === "compact";
1380
+ if (isCompactCommand) {
1381
+ types$1.logger.debug("[claudeRemote] Compaction started");
1382
+ if (opts.onCompletionEvent) {
1383
+ opts.onCompletionEvent("Compaction started");
1384
+ }
1385
+ }
1342
1386
  let message = new PushableAsyncIterable();
1343
1387
  message.push({
1344
1388
  type: "user",
@@ -1378,12 +1422,6 @@ async function claudeRemote(opts) {
1378
1422
  types$1.logger.debug(`[claudeRemote] Session file found: ${systemInit.session_id} ${found}`);
1379
1423
  opts.onSessionFound(systemInit.session_id);
1380
1424
  }
1381
- if (isCompactCommand) {
1382
- types$1.logger.debug("[claudeRemote] Compaction started");
1383
- if (opts.onCompletionEvent) {
1384
- opts.onCompletionEvent("Compaction started");
1385
- }
1386
- }
1387
1425
  }
1388
1426
  if (message2.type === "result") {
1389
1427
  updateThinking(false);
@@ -2091,9 +2129,6 @@ async function claudeRemoteLauncher(session) {
2091
2129
  sessionId: session.sessionId,
2092
2130
  workingDirectory: session.path,
2093
2131
  onMessage: (message) => {
2094
- if (message.type === "summary") {
2095
- session.client.sendClaudeSessionMessage(message);
2096
- }
2097
2132
  }
2098
2133
  });
2099
2134
  let exitReason = null;
@@ -2253,8 +2288,8 @@ async function claudeRemoteLauncher(session) {
2253
2288
  model: messageData.mode.model,
2254
2289
  fallbackModel: messageData.mode.fallbackModel,
2255
2290
  customSystemPrompt: messageData.mode.customSystemPrompt,
2256
- appendSystemPrompt: messageData.mode.appendSystemPrompt,
2257
- allowedTools: messageData.mode.allowedTools,
2291
+ appendSystemPrompt: messageData.mode.appendSystemPrompt ? messageData.mode.appendSystemPrompt + "\n" + systemPrompt : systemPrompt,
2292
+ allowedTools: messageData.mode.allowedTools ? [...messageData.mode.allowedTools, ...session.allowedTools ? session.allowedTools : []] : session.allowedTools ? [...session.allowedTools] : void 0,
2258
2293
  disallowedTools: messageData.mode.disallowedTools,
2259
2294
  onSessionFound: (sessionId) => {
2260
2295
  sdkToLogConverter.updateSessionId(sessionId);
@@ -2330,6 +2365,7 @@ async function loop(opts) {
2330
2365
  mcpServers: opts.mcpServers,
2331
2366
  logPath,
2332
2367
  messageQueue: opts.messageQueue,
2368
+ allowedTools: opts.allowedTools,
2333
2369
  onModeChange: opts.onModeChange
2334
2370
  });
2335
2371
  if (opts.onSessionReady) {
@@ -2364,7 +2400,7 @@ async function loop(opts) {
2364
2400
  }
2365
2401
 
2366
2402
  var name = "happy-coder";
2367
- var version = "0.7.2";
2403
+ var version = "0.8.0";
2368
2404
  var description = "Claude Code session sharing CLI";
2369
2405
  var author = "Kirill Dubovitskiy";
2370
2406
  var license = "MIT";
@@ -2414,7 +2450,7 @@ var scripts = {
2414
2450
  test: "yarn build && vitest run",
2415
2451
  "test:watch": "vitest",
2416
2452
  "test:integration-test-env": "yarn build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
2417
- dev: "yarn build && npx tsx src/index.ts",
2453
+ dev: "DEBUG=1 yarn build && npx tsx src/index.ts",
2418
2454
  "dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
2419
2455
  "dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
2420
2456
  prepublishOnly: "yarn build && yarn test",
@@ -4323,6 +4359,88 @@ async function startDaemon() {
4323
4359
  }
4324
4360
  }
4325
4361
 
4362
+ async function startHappyServer(client) {
4363
+ const handler = async (title) => {
4364
+ types$1.logger.debug("[happyMCP] Changing title to:", title);
4365
+ try {
4366
+ client.sendClaudeSessionMessage({
4367
+ type: "summary",
4368
+ summary: title,
4369
+ leafUuid: node_crypto.randomUUID()
4370
+ });
4371
+ return { success: true };
4372
+ } catch (error) {
4373
+ return { success: false, error: String(error) };
4374
+ }
4375
+ };
4376
+ const mcp = new mcp_js.McpServer({
4377
+ name: "Happy MCP",
4378
+ version: "1.0.0",
4379
+ description: "Happy CLI MCP server with chat session management tools"
4380
+ });
4381
+ mcp.registerTool("change_title", {
4382
+ description: "Change the title of the current chat session",
4383
+ title: "Change Chat Title",
4384
+ inputSchema: {
4385
+ title: z.z.string().describe("The new title for the chat session")
4386
+ }
4387
+ }, async (args) => {
4388
+ const response = await handler(args.title);
4389
+ types$1.logger.debug("[happyMCP] Response:", response);
4390
+ if (response.success) {
4391
+ return {
4392
+ content: [
4393
+ {
4394
+ type: "text",
4395
+ text: `Successfully changed chat title to: "${args.title}"`
4396
+ }
4397
+ ],
4398
+ isError: false
4399
+ };
4400
+ } else {
4401
+ return {
4402
+ content: [
4403
+ {
4404
+ type: "text",
4405
+ text: `Failed to change chat title: ${response.error || "Unknown error"}`
4406
+ }
4407
+ ],
4408
+ isError: true
4409
+ };
4410
+ }
4411
+ });
4412
+ const transport = new streamableHttp_js.StreamableHTTPServerTransport({
4413
+ // NOTE: Returning session id here will result in claude
4414
+ // sdk spawn to fail with `Invalid Request: Server already initialized`
4415
+ sessionIdGenerator: void 0
4416
+ });
4417
+ await mcp.connect(transport);
4418
+ const server = node_http.createServer(async (req, res) => {
4419
+ try {
4420
+ await transport.handleRequest(req, res);
4421
+ } catch (error) {
4422
+ types$1.logger.debug("Error handling request:", error);
4423
+ if (!res.headersSent) {
4424
+ res.writeHead(500).end();
4425
+ }
4426
+ }
4427
+ });
4428
+ const baseUrl = await new Promise((resolve) => {
4429
+ server.listen(0, "127.0.0.1", () => {
4430
+ const addr = server.address();
4431
+ resolve(new URL(`http://127.0.0.1:${addr.port}`));
4432
+ });
4433
+ });
4434
+ return {
4435
+ url: baseUrl.toString(),
4436
+ toolNames: ["change_title"],
4437
+ stop: () => {
4438
+ mcp.close();
4439
+ server.close();
4440
+ }
4441
+ };
4442
+ }
4443
+
4326
4444
  async function start(credentials, options = {}) {
4327
4445
  const workingDirectory = process.cwd();
4328
4446
  const sessionTag = node_crypto.randomUUID();
@@ -4382,6 +4500,8 @@ async function start(credentials, options = {}) {
4382
4500
  }
4383
4501
  });
4384
4502
  const session = api.sessionSyncClient(response);
4503
+ const happyServer = await startHappyServer(session);
4504
+ types$1.logger.debug(`[START] Happy MCP server started at ${happyServer.url}`);
4385
4505
  const logPath = await types$1.logger.logFilePathPromise;
4386
4506
  types$1.logger.infoDeveloper(`Session: ${response.id}`);
4387
4507
  types$1.logger.infoDeveloper(`Logs: ${logPath}`);
@@ -4516,6 +4636,7 @@ async function start(credentials, options = {}) {
4516
4636
  await session.close();
4517
4637
  }
4518
4638
  stopCaffeinate();
4639
+ happyServer.stop();
4519
4640
  types$1.logger.debug("[START] Cleanup complete, exiting");
4520
4641
  process.exit(0);
4521
4642
  } catch (error) {
@@ -4540,6 +4661,7 @@ async function start(credentials, options = {}) {
4540
4661
  startingMode: options.startingMode,
4541
4662
  messageQueue,
4542
4663
  api,
4664
+ allowedTools: happyServer.toolNames.map((toolName) => `mcp__happy__${toolName}`),
4543
4665
  onModeChange: (newMode) => {
4544
4666
  session.sendSessionEvent({ type: "switch", mode: newMode });
4545
4667
  session.updateAgentState((currentState) => ({
@@ -4549,7 +4671,12 @@ async function start(credentials, options = {}) {
4549
4671
  },
4550
4672
  onSessionReady: (sessionInstance) => {
4551
4673
  },
4552
- mcpServers: {},
4674
+ mcpServers: {
4675
+ "happy": {
4676
+ type: "http",
4677
+ url: happyServer.url
4678
+ }
4679
+ },
4553
4680
  session,
4554
4681
  claudeEnvVars: options.claudeEnvVars,
4555
4682
  claudeArgs: options.claudeArgs
@@ -4561,28 +4688,11 @@ async function start(credentials, options = {}) {
4561
4688
  await session.close();
4562
4689
  stopCaffeinate();
4563
4690
  types$1.logger.debug("Stopped sleep prevention");
4691
+ happyServer.stop();
4692
+ types$1.logger.debug("Stopped Happy MCP server");
4564
4693
  process.exit(0);
4565
4694
  }
4566
4695
 
4567
- function trimIdent(text) {
4568
- const lines = text.split("\n");
4569
- while (lines.length > 0 && lines[0].trim() === "") {
4570
- lines.shift();
4571
- }
4572
- while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
4573
- lines.pop();
4574
- }
4575
- const minSpaces = lines.reduce((min, line) => {
4576
- if (line.trim() === "") {
4577
- return min;
4578
- }
4579
- const leadingSpaces = line.match(/^\s*/)[0].length;
4580
- return Math.min(min, leadingSpaces);
4581
- }, Infinity);
4582
- const trimmedLines = lines.map((line) => line.slice(minSpaces));
4583
- return trimmedLines.join("\n");
4584
- }
4585
-
4586
4696
  const PLIST_LABEL$1 = "com.happy-cli.daemon";
4587
4697
  const PLIST_FILE$1 = `/Library/LaunchDaemons/${PLIST_LABEL$1}.plist`;
4588
4698
  async function install$1() {
package/dist/index.mjs CHANGED
@@ -42,6 +42,7 @@ class Session {
42
42
  claudeEnvVars;
43
43
  claudeArgs;
44
44
  mcpServers;
45
+ allowedTools;
45
46
  _onModeChange;
46
47
  sessionId;
47
48
  mode = "local";
@@ -56,6 +57,7 @@ class Session {
56
57
  this.claudeEnvVars = opts.claudeEnvVars;
57
58
  this.claudeArgs = opts.claudeArgs;
58
59
  this.mcpServers = opts.mcpServers;
60
+ this.allowedTools = opts.allowedTools;
59
61
  this._onModeChange = opts.onModeChange;
60
62
  this.client.keepAlive(this.thinking, this.mode);
61
63
  setInterval(() => {
@@ -114,6 +116,29 @@ function projectPath() {
114
116
  return path;
115
117
  }
116
118
 
119
+ function trimIdent(text) {
120
+ const lines = text.split("\n");
121
+ while (lines.length > 0 && lines[0].trim() === "") {
122
+ lines.shift();
123
+ }
124
+ while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
125
+ lines.pop();
126
+ }
127
+ const minSpaces = lines.reduce((min, line) => {
128
+ if (line.trim() === "") {
129
+ return min;
130
+ }
131
+ const leadingSpaces = line.match(/^\s*/)[0].length;
132
+ return Math.min(min, leadingSpaces);
133
+ }, Infinity);
134
+ const trimmedLines = lines.map((line) => line.slice(minSpaces));
135
+ return trimmedLines.join("\n");
136
+ }
137
+
138
+ const systemPrompt = trimIdent(`
139
+ ALWAYS when you start a new chat - you must call a tool "mcp__happy__change_title" to set a chat title. When you think chat title is not relevant anymore - call the tool again to change it. When chat name is too generic and you have a change to make it more specific - call the tool again to change it. This title is needed to easily find the chat in the future. Help human.
140
+ `);
141
+
117
142
  dirname$1(fileURLToPath$1(import.meta.url));
118
143
  async function claudeLocal(opts) {
119
144
  const projectDir = getProjectPath(opts.path);
@@ -161,6 +186,13 @@ async function claudeLocal(opts) {
161
186
  if (startFrom) {
162
187
  args.push("--resume", startFrom);
163
188
  }
189
+ args.push("--append-system-prompt", systemPrompt);
190
+ if (opts.mcpServers && Object.keys(opts.mcpServers).length > 0) {
191
+ args.push("--mcp-config", JSON.stringify({ mcpServers: opts.mcpServers }));
192
+ }
193
+ if (opts.allowedTools && opts.allowedTools.length > 0) {
194
+ args.push("--allowedTools", opts.allowedTools.join(","));
195
+ }
164
196
  if (opts.claudeArgs) {
165
197
  args.push(...opts.claudeArgs);
166
198
  }
@@ -505,7 +537,9 @@ async function claudeLocalLauncher(session) {
505
537
  sessionId: session.sessionId,
506
538
  workingDirectory: session.path,
507
539
  onMessage: (message) => {
508
- session.client.sendClaudeSessionMessage(message);
540
+ if (message.type !== "summary") {
541
+ session.client.sendClaudeSessionMessage(message);
542
+ }
509
543
  }
510
544
  });
511
545
  let exitReason = null;
@@ -535,7 +569,9 @@ async function claudeLocalLauncher(session) {
535
569
  }
536
570
  session.client.setHandler("abort", doAbort);
537
571
  session.client.setHandler("switch", doSwitch);
538
- session.queue.setOnMessage(doSwitch);
572
+ session.queue.setOnMessage((message, mode) => {
573
+ doSwitch();
574
+ });
539
575
  if (session.queue.size() > 0) {
540
576
  return "switch";
541
577
  }
@@ -556,7 +592,9 @@ async function claudeLocalLauncher(session) {
556
592
  onThinkingChange: session.onThinkingChange,
557
593
  abort: processAbortController.signal,
558
594
  claudeEnvVars: session.claudeEnvVars,
559
- claudeArgs: session.claudeArgs
595
+ claudeArgs: session.claudeArgs,
596
+ mcpServers: session.mcpServers,
597
+ allowedTools: session.allowedTools
560
598
  });
561
599
  if (!exitReason) {
562
600
  exitReason = "exit";
@@ -1318,6 +1356,12 @@ async function claudeRemote(opts) {
1318
1356
  logger.debug("[claudeRemote] /compact command detected - will process as normal but with compaction behavior");
1319
1357
  }
1320
1358
  const isCompactCommand = specialCommand.type === "compact";
1359
+ if (isCompactCommand) {
1360
+ logger.debug("[claudeRemote] Compaction started");
1361
+ if (opts.onCompletionEvent) {
1362
+ opts.onCompletionEvent("Compaction started");
1363
+ }
1364
+ }
1321
1365
  let message = new PushableAsyncIterable();
1322
1366
  message.push({
1323
1367
  type: "user",
@@ -1357,12 +1401,6 @@ async function claudeRemote(opts) {
1357
1401
  logger.debug(`[claudeRemote] Session file found: ${systemInit.session_id} ${found}`);
1358
1402
  opts.onSessionFound(systemInit.session_id);
1359
1403
  }
1360
- if (isCompactCommand) {
1361
- logger.debug("[claudeRemote] Compaction started");
1362
- if (opts.onCompletionEvent) {
1363
- opts.onCompletionEvent("Compaction started");
1364
- }
1365
- }
1366
1404
  }
1367
1405
  if (message2.type === "result") {
1368
1406
  updateThinking(false);
@@ -2070,9 +2108,6 @@ async function claudeRemoteLauncher(session) {
2070
2108
  sessionId: session.sessionId,
2071
2109
  workingDirectory: session.path,
2072
2110
  onMessage: (message) => {
2073
- if (message.type === "summary") {
2074
- session.client.sendClaudeSessionMessage(message);
2075
- }
2076
2111
  }
2077
2112
  });
2078
2113
  let exitReason = null;
@@ -2232,8 +2267,8 @@ async function claudeRemoteLauncher(session) {
2232
2267
  model: messageData.mode.model,
2233
2268
  fallbackModel: messageData.mode.fallbackModel,
2234
2269
  customSystemPrompt: messageData.mode.customSystemPrompt,
2235
- appendSystemPrompt: messageData.mode.appendSystemPrompt,
2236
- allowedTools: messageData.mode.allowedTools,
2270
+ appendSystemPrompt: messageData.mode.appendSystemPrompt ? messageData.mode.appendSystemPrompt + "\n" + systemPrompt : systemPrompt,
2271
+ allowedTools: messageData.mode.allowedTools ? [...messageData.mode.allowedTools, ...session.allowedTools ? session.allowedTools : []] : session.allowedTools ? [...session.allowedTools] : void 0,
2237
2272
  disallowedTools: messageData.mode.disallowedTools,
2238
2273
  onSessionFound: (sessionId) => {
2239
2274
  sdkToLogConverter.updateSessionId(sessionId);
@@ -2309,6 +2344,7 @@ async function loop(opts) {
2309
2344
  mcpServers: opts.mcpServers,
2310
2345
  logPath,
2311
2346
  messageQueue: opts.messageQueue,
2347
+ allowedTools: opts.allowedTools,
2312
2348
  onModeChange: opts.onModeChange
2313
2349
  });
2314
2350
  if (opts.onSessionReady) {
@@ -2343,7 +2379,7 @@ async function loop(opts) {
2343
2379
  }
2344
2380
 
2345
2381
  var name = "happy-coder";
2346
- var version = "0.7.2";
2382
+ var version = "0.8.0";
2347
2383
  var description = "Claude Code session sharing CLI";
2348
2384
  var author = "Kirill Dubovitskiy";
2349
2385
  var license = "MIT";
@@ -2393,7 +2429,7 @@ var scripts = {
2393
2429
  test: "yarn build && vitest run",
2394
2430
  "test:watch": "vitest",
2395
2431
  "test:integration-test-env": "yarn build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
2396
- dev: "yarn build && npx tsx src/index.ts",
2432
+ dev: "DEBUG=1 yarn build && npx tsx src/index.ts",
2397
2433
  "dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
2398
2434
  "dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
2399
2435
  prepublishOnly: "yarn build && yarn test",
@@ -4302,6 +4338,88 @@ async function startDaemon() {
4302
4338
  }
4303
4339
  }
4304
4340
 
4341
+ async function startHappyServer(client) {
4342
+ const handler = async (title) => {
4343
+ logger.debug("[happyMCP] Changing title to:", title);
4344
+ try {
4345
+ client.sendClaudeSessionMessage({
4346
+ type: "summary",
4347
+ summary: title,
4348
+ leafUuid: randomUUID()
4349
+ });
4350
+ return { success: true };
4351
+ } catch (error) {
4352
+ return { success: false, error: String(error) };
4353
+ }
4354
+ };
4355
+ const mcp = new McpServer({
4356
+ name: "Happy MCP",
4357
+ version: "1.0.0",
4358
+ description: "Happy CLI MCP server with chat session management tools"
4359
+ });
4360
+ mcp.registerTool("change_title", {
4361
+ description: "Change the title of the current chat session",
4362
+ title: "Change Chat Title",
4363
+ inputSchema: {
4364
+ title: z$1.string().describe("The new title for the chat session")
4365
+ }
4366
+ }, async (args) => {
4367
+ const response = await handler(args.title);
4368
+ logger.debug("[happyMCP] Response:", response);
4369
+ if (response.success) {
4370
+ return {
4371
+ content: [
4372
+ {
4373
+ type: "text",
4374
+ text: `Successfully changed chat title to: "${args.title}"`
4375
+ }
4376
+ ],
4377
+ isError: false
4378
+ };
4379
+ } else {
4380
+ return {
4381
+ content: [
4382
+ {
4383
+ type: "text",
4384
+ text: `Failed to change chat title: ${response.error || "Unknown error"}`
4385
+ }
4386
+ ],
4387
+ isError: true
4388
+ };
4389
+ }
4390
+ });
4391
+ const transport = new StreamableHTTPServerTransport({
4392
+ // NOTE: Returning session id here will result in claude
4393
+ // sdk spawn to fail with `Invalid Request: Server already initialized`
4394
+ sessionIdGenerator: void 0
4395
+ });
4396
+ await mcp.connect(transport);
4397
+ const server = createServer(async (req, res) => {
4398
+ try {
4399
+ await transport.handleRequest(req, res);
4400
+ } catch (error) {
4401
+ logger.debug("Error handling request:", error);
4402
+ if (!res.headersSent) {
4403
+ res.writeHead(500).end();
4404
+ }
4405
+ }
4406
+ });
4407
+ const baseUrl = await new Promise((resolve) => {
4408
+ server.listen(0, "127.0.0.1", () => {
4409
+ const addr = server.address();
4410
+ resolve(new URL(`http://127.0.0.1:${addr.port}`));
4411
+ });
4412
+ });
4413
+ return {
4414
+ url: baseUrl.toString(),
4415
+ toolNames: ["change_title"],
4416
+ stop: () => {
4417
+ mcp.close();
4418
+ server.close();
4419
+ }
4420
+ };
4421
+ }
4422
+
4305
4423
  async function start(credentials, options = {}) {
4306
4424
  const workingDirectory = process.cwd();
4307
4425
  const sessionTag = randomUUID();
@@ -4361,6 +4479,8 @@ async function start(credentials, options = {}) {
4361
4479
  }
4362
4480
  });
4363
4481
  const session = api.sessionSyncClient(response);
4482
+ const happyServer = await startHappyServer(session);
4483
+ logger.debug(`[START] Happy MCP server started at ${happyServer.url}`);
4364
4484
  const logPath = await logger.logFilePathPromise;
4365
4485
  logger.infoDeveloper(`Session: ${response.id}`);
4366
4486
  logger.infoDeveloper(`Logs: ${logPath}`);
@@ -4495,6 +4615,7 @@ async function start(credentials, options = {}) {
4495
4615
  await session.close();
4496
4616
  }
4497
4617
  stopCaffeinate();
4618
+ happyServer.stop();
4498
4619
  logger.debug("[START] Cleanup complete, exiting");
4499
4620
  process.exit(0);
4500
4621
  } catch (error) {
@@ -4519,6 +4640,7 @@ async function start(credentials, options = {}) {
4519
4640
  startingMode: options.startingMode,
4520
4641
  messageQueue,
4521
4642
  api,
4643
+ allowedTools: happyServer.toolNames.map((toolName) => `mcp__happy__${toolName}`),
4522
4644
  onModeChange: (newMode) => {
4523
4645
  session.sendSessionEvent({ type: "switch", mode: newMode });
4524
4646
  session.updateAgentState((currentState) => ({
@@ -4528,7 +4650,12 @@ async function start(credentials, options = {}) {
4528
4650
  },
4529
4651
  onSessionReady: (sessionInstance) => {
4530
4652
  },
4531
- mcpServers: {},
4653
+ mcpServers: {
4654
+ "happy": {
4655
+ type: "http",
4656
+ url: happyServer.url
4657
+ }
4658
+ },
4532
4659
  session,
4533
4660
  claudeEnvVars: options.claudeEnvVars,
4534
4661
  claudeArgs: options.claudeArgs
@@ -4540,28 +4667,11 @@ async function start(credentials, options = {}) {
4540
4667
  await session.close();
4541
4668
  stopCaffeinate();
4542
4669
  logger.debug("Stopped sleep prevention");
4670
+ happyServer.stop();
4671
+ logger.debug("Stopped Happy MCP server");
4543
4672
  process.exit(0);
4544
4673
  }
4545
4674
 
4546
- function trimIdent(text) {
4547
- const lines = text.split("\n");
4548
- while (lines.length > 0 && lines[0].trim() === "") {
4549
- lines.shift();
4550
- }
4551
- while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
4552
- lines.pop();
4553
- }
4554
- const minSpaces = lines.reduce((min, line) => {
4555
- if (line.trim() === "") {
4556
- return min;
4557
- }
4558
- const leadingSpaces = line.match(/^\s*/)[0].length;
4559
- return Math.min(min, leadingSpaces);
4560
- }, Infinity);
4561
- const trimmedLines = lines.map((line) => line.slice(minSpaces));
4562
- return trimmedLines.join("\n");
4563
- }
4564
-
4565
4675
  const PLIST_LABEL$1 = "com.happy-cli.daemon";
4566
4676
  const PLIST_FILE$1 = `/Library/LaunchDaemons/${PLIST_LABEL$1}.plist`;
4567
4677
  async function install$1() {
package/dist/lib.d.cts CHANGED
@@ -751,5 +751,4 @@ declare class Configuration {
751
751
  }
752
752
  declare const configuration: Configuration;
753
753
 
754
- export { ApiClient, ApiSessionClient, RawJSONLinesSchema, configuration, logger };
755
- export type { RawJSONLines };
754
+ export { ApiClient, ApiSessionClient, type RawJSONLines, RawJSONLinesSchema, configuration, logger };
package/dist/lib.d.mts CHANGED
@@ -751,5 +751,4 @@ declare class Configuration {
751
751
  }
752
752
  declare const configuration: Configuration;
753
753
 
754
- export { ApiClient, ApiSessionClient, RawJSONLinesSchema, configuration, logger };
755
- export type { RawJSONLines };
754
+ export { ApiClient, ApiSessionClient, type RawJSONLines, RawJSONLinesSchema, configuration, logger };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happy-coder",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "Claude Code session sharing CLI",
5
5
  "author": "Kirill Dubovitskiy",
6
6
  "license": "MIT",
@@ -50,7 +50,7 @@
50
50
  "test": "yarn build && vitest run",
51
51
  "test:watch": "vitest",
52
52
  "test:integration-test-env": "yarn build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
53
- "dev": "yarn build && npx tsx src/index.ts",
53
+ "dev": "DEBUG=1 yarn build && npx tsx src/index.ts",
54
54
  "dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
55
55
  "dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
56
56
  "prepublishOnly": "yarn build && yarn test",