Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,32 +68,32 @@
"postinstall": "node scripts/unpack-tools.cjs"
},
"dependencies": {
"@anthropic-ai/claude-code": "2.0.24",
"@anthropic-ai/sdk": "0.65.0",
"@modelcontextprotocol/sdk": "^1.15.1",
"@anthropic-ai/claude-code": "^2.0.42",
"@anthropic-ai/sdk": "^0.69.0",
"@modelcontextprotocol/sdk": "^1.22.0",
"@stablelib/base64": "^2.0.1",
"@stablelib/hex": "^2.0.1",
"@types/cross-spawn": "^6.0.6",
"@types/http-proxy": "^1.17.16",
"@types/ps-list": "^6.2.1",
"@types/qrcode-terminal": "^0.12.2",
"@types/react": "^19.1.9",
"@types/react": "^19.2.5",
"@types/tmp": "^0.2.6",
"axios": "^1.10.0",
"chalk": "^5.4.1",
"axios": "^1.13.2",
"chalk": "^5.6.2",
"cross-spawn": "^7.0.6",
"expo-server-sdk": "^3.15.0",
"fastify": "^5.5.0",
"fastify": "^5.6.2",
"fastify-type-provider-zod": "4.0.2",
"http-proxy": "^1.18.1",
"http-proxy-middleware": "^3.0.5",
"ink": "^6.1.0",
"ink": "^6.5.0",
"open": "^10.2.0",
"ps-list": "^8.1.1",
"qrcode-terminal": "^0.12.0",
"react": "^19.1.1",
"react": "^19.2.0",
"socket.io-client": "^4.8.1",
"tar": "^7.4.3",
"tar": "^7.5.2",
"tmp": "^0.2.5",
"tweetnacl": "^1.0.3",
"zod": "^3.23.8"
Expand All @@ -103,20 +103,21 @@
"@types/node": ">=20",
"cross-env": "^10.0.0",
"dotenv": "^16.6.1",
"eslint": "^9",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10",
"pkgroll": "^2.14.2",
"release-it": "^19.0.4",
"shx": "^0.3.3",
"ts-node": "^10",
"tsx": "^4.20.3",
"typescript": "^5",
"vitest": "^3.2.4"
"tsx": "^4.20.6",
"typescript": "^5.9.3",
"vitest": "^4.0.9"
},
"resolutions": {
"whatwg-url": "14.2.0",
"parse-path": "7.0.3",
"@types/parse-path": "7.0.3"
"@types/parse-path": "7.0.3",
"js-yaml": "^4.1.1"
},
"publishConfig": {
"registry": "https://registry.npmjs.org"
Expand Down
71 changes: 71 additions & 0 deletions scripts/claude_code_paths.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Shared configuration for Claude Code CLI paths
*
* This module provides the canonical list of paths/module specifiers
* for locating the Claude Code CLI across different versions and installations.
*
* Used by:
* - claude_local_launcher.cjs (dynamic imports)
* - claude_remote_launcher.cjs (dynamic imports)
* - src/claude/sdk/utils.ts (filesystem paths - PATH_SEGMENTS duplicated there)
*
* NOTE: PATH_SEGMENTS is intentionally duplicated in src/claude/sdk/utils.ts
* The duplication is acceptable because:
* - This file is a standalone CJS launcher script
* - The TS file is bundled application code
* - They execute in different contexts but must agree on the paths to try
* - If Claude Code changes directory structure, update both locations
*/

/**
* Module specifiers for dynamic import() calls
* Ordered by preference - try each in sequence
*/
const MODULE_SPECIFIERS = [
'@anthropic-ai/claude-code/cli.js', // Standard location
'@anthropic-ai/claude-code', // Package root (fallback)
'@anthropic-ai/claude-code/dist/cli.js' // Build output location
];

/**
* Relative path segments within node_modules/@anthropic-ai/claude-code/
* Used for constructing filesystem paths
* Ordered by preference - try each in sequence
*/
const PATH_SEGMENTS = [
'cli.js', // Standard location
'bin/cli.js', // Alternative bin location
'dist/cli.js' // Build output location
];

/**
* Attempts to import Claude Code CLI using fallback module specifiers
* Tries each specifier in order until one succeeds
* Exits process with error if all attempts fail
* @returns {Promise<void>} Resolves when module is successfully loaded
*/
async function loadClaudeCodeCli() {
const errors = [];

for (const specifier of MODULE_SPECIFIERS) {
try {
await import(specifier);
return Promise.resolve(); // Success! Exit the function
} catch (error) {
errors.push({ specifier, error });
}
}

// If we get here, all attempts failed
console.error('Failed to load Claude Code CLI. Tried:');
for (const { specifier, error } of errors) {
console.error(` - ${specifier}: ${error.message}`);
}
process.exit(1);
}

module.exports = {
MODULE_SPECIFIERS,
PATH_SEGMENTS,
loadClaudeCodeCli
};
7 changes: 6 additions & 1 deletion scripts/claude_local_launcher.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,9 @@ global.fetch = function(...args) {
Object.defineProperty(global.fetch, 'name', { value: 'fetch' });
Object.defineProperty(global.fetch, 'length', { value: originalFetch.length });

import('@anthropic-ai/claude-code/cli.js')
// Load Claude Code CLI with shared import logic
const { loadClaudeCodeCli } = require('./claude_code_paths.cjs');
loadClaudeCodeCli().catch(err => {
console.error('Unexpected error loading Claude Code CLI:', err);
process.exit(1);
});
7 changes: 6 additions & 1 deletion scripts/claude_remote_launcher.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,9 @@ global.setTimeout = function(callback, delay, ...args) {
Object.defineProperty(global.setTimeout, 'name', { value: 'setTimeout' });
Object.defineProperty(global.setTimeout, 'length', { value: originalSetTimeout.length });

import('@anthropic-ai/claude-code/cli.js')
// Load Claude Code CLI with shared import logic
const { loadClaudeCodeCli } = require('./claude_code_paths.cjs');
loadClaudeCodeCli().catch(err => {
console.error('Unexpected error loading Claude Code CLI:', err);
process.exit(1);
});
55 changes: 55 additions & 0 deletions src/claude/sdk/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, it, expect } from 'vitest';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

/**
* Tests for Claude Code SDK utilities
*/

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

describe('PATH_SEGMENTS synchronization', () => {
it('should match between CJS (scripts/claude_code_paths.cjs) and TS (src/claude/sdk/utils.ts)', async () => {
// Import the CJS module to get its PATH_SEGMENTS
const cjsModulePath = join(__dirname, '../../../scripts/claude_code_paths.cjs');
const cjsModule = await import(cjsModulePath);
const cjsPathSegments = cjsModule.PATH_SEGMENTS;

// Define the TS PATH_SEGMENTS (duplicated from src/claude/sdk/utils.ts)
const tsPathSegments = [
'cli.js', // Standard location
'bin/cli.js', // Alternative bin location
'dist/cli.js' // Build output location
];

// Verify they match
expect(cjsPathSegments).toEqual(tsPathSegments);
expect(cjsPathSegments.length).toBe(tsPathSegments.length);

// Also verify individual elements for better error messages
for (let i = 0; i < tsPathSegments.length; i++) {
expect(cjsPathSegments[i]).toBe(tsPathSegments[i]);
}
});

it('PATH_SEGMENTS should have expected structure', async () => {
const cjsModulePath = join(__dirname, '../../../scripts/claude_code_paths.cjs');
const cjsModule = await import(cjsModulePath);
const pathSegments = cjsModule.PATH_SEGMENTS;

// Verify it's an array with at least one entry
expect(Array.isArray(pathSegments)).toBe(true);
expect(pathSegments.length).toBeGreaterThan(0);

// Verify all entries are strings
pathSegments.forEach((segment: unknown) => {
expect(typeof segment).toBe('string');
});

// Verify all entries end with .js
pathSegments.forEach((segment: string) => {
expect(segment.endsWith('.js')).toBe(true);
});
});
});
28 changes: 27 additions & 1 deletion src/claude/sdk/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,25 @@

import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { existsSync } from 'node:fs'
import { logger } from '@/ui/logger'

/**
* Path segments within node_modules/@anthropic-ai/claude-code/
*
* NOTE: Intentionally duplicated from scripts/claude_code_paths.cjs
* The duplication is acceptable because:
* - scripts/ contains standalone launcher scripts (CJS, executed directly)
* - src/ contains bundled application code (ESM, bundled by pkgroll)
* - They execute in different contexts but must agree on paths to try
* - If Claude Code changes directory structure, update both locations
*/
const PATH_SEGMENTS = [
'cli.js', // Standard location
'bin/cli.js', // Alternative bin location
'dist/cli.js' // Build output location
] as const

/**
* Get the directory path of the current module
*/
Expand All @@ -17,7 +34,16 @@ const __dirname = join(__filename, '..')
* Get default path to Claude Code executable
*/
export function getDefaultClaudeCodePath(): string {
return join(__dirname, '..', '..', '..', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js')
const base = join(__dirname, '..', '..', '..')
const nodeModulesBase = join(base, 'node_modules', '@anthropic-ai', 'claude-code')

// Build candidates from shared PATH_SEGMENTS
const candidates = (PATH_SEGMENTS as readonly string[]).map(segment => join(nodeModulesBase, segment))

for (const p of candidates) {
if (existsSync(p)) return p
}
return candidates[0]
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/claude/utils/startHappyServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ export async function startHappyServer(client: ApiSessionClient) {

//
// Create the MCP server
// Note: 'description' field removed in @modelcontextprotocol/sdk 1.0.0+
//

const mcp = new McpServer({
name: "Happy MCP",
version: "1.0.0",
description: "Happy CLI MCP server with chat session management tools",
});

mcp.registerTool('change_title', {
Expand Down
4 changes: 3 additions & 1 deletion src/codex/codexMcpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,11 @@ export class CodexMcpClient {
private permissionHandler: CodexPermissionHandler | null = null;

constructor() {
// Note: MCP SDK 1.0.0+ removed 'description' field and 'tools: {}' capability
// Capabilities are now negotiated during connection handshake
this.client = new Client(
{ name: 'happy-codex-client', version: '1.0.0' },
{ capabilities: { tools: {}, elicitation: {} } }
{ capabilities: { elicitation: {} } }
);

this.client.setNotificationHandler(z.object({
Expand Down
5 changes: 3 additions & 2 deletions src/codex/happyMcpStdioBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@ async function main() {

async function ensureHttpClient(): Promise<Client> {
if (httpClient) return httpClient;
// Note: 'tools: {}' capability removed in @modelcontextprotocol/sdk 1.0.0+
const client = new Client(
{ name: 'happy-stdio-bridge', version: '1.0.0' },
{ capabilities: { tools: {} } }
{ capabilities: {} }
);

const transport = new StreamableHTTPClientTransport(new URL(baseUrl));
Expand All @@ -58,10 +59,10 @@ async function main() {
}

// Create STDIO MCP server
// Note: 'description' field removed in @modelcontextprotocol/sdk 1.0.0+
const server = new McpServer({
name: 'Happy MCP Bridge',
version: '1.0.0',
description: 'STDIO bridge forwarding to Happy HTTP MCP',
});

// Register the single tool and forward to HTTP MCP
Expand Down
9 changes: 6 additions & 3 deletions src/utils/MessageQueue.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { SDKMessage, SDKUserMessage } from "@anthropic-ai/claude-code";
// Note: Import from local types instead of @anthropic-ai/claude-code SDK
// This decouples MessageQueue from SDK internals and allows us to maintain
// stable type definitions even if SDK types change between versions
import { SDKUserMessage } from "@/claude/sdk/types";
import { logger } from "@/ui/logger";

/**
Expand Down Expand Up @@ -36,7 +39,7 @@ export class MessageQueue implements AsyncIterable<SDKUserMessage> {
role: 'user',
content: message,
},
parent_tool_use_id: null,
parent_tool_use_id: undefined,
session_id: '',
});
} else {
Expand All @@ -47,7 +50,7 @@ export class MessageQueue implements AsyncIterable<SDKUserMessage> {
role: 'user',
content: message,
},
parent_tool_use_id: null,
parent_tool_use_id: undefined,
session_id: '',
});
}
Expand Down
Loading
Loading