Skip to content
Merged
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
6 changes: 3 additions & 3 deletions src/__tests__/codex-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ describe('CodxAdapter.writeMcpConfig()', () => {
let adapter: CodxAdapter;
beforeEach(() => { adapter = new CodxAdapter(); vi.clearAllMocks(); });

it('writes config.toml with [mcp_servers.custena] url and bearer_token', async () => {
it('writes config.toml with [mcp_servers.custena] url (no bearer_token — not supported by streamable_http)', async () => {
vi.mocked(fs.readFile).mockRejectedValueOnce(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
Expand All @@ -156,8 +156,8 @@ describe('CodxAdapter.writeMcpConfig()', () => {
expect(writePath).toBe(CONFIG_PATH);
expect(content).toContain('[mcp_servers.custena]');
expect(content).toContain(`url = ${JSON.stringify(MCP_URL)}`);
expect(content).toContain('bearer_token = "test-access-token"');
expect(content).toContain('default_tools_approval_mode = "approve"');
expect(content).not.toContain('bearer_token');
expect(content).not.toContain('default_tools_approval_mode');
});

it('preserves existing non-custena TOML content', async () => {
Expand Down
11 changes: 8 additions & 3 deletions src/adapters/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export class CodxAdapter implements HostAdapter {
id = 'codex';
displayName = 'OpenAI Codex';
capabilities = { mcpPrompts: true, hooks: false };
postInstallNote = 'Run `codex mcp login custena` to authenticate.';

private get configDir() { return path.join(os.homedir(), '.codex'); }
private get configPath() { return path.join(this.configDir, 'config.toml'); }
Expand All @@ -82,12 +83,16 @@ export class CodxAdapter implements HostAdapter {
return { installed: false };
}

async writeMcpConfig(oauth: OAuthConfig): Promise<void> {
async writeMcpConfig(_oauth: OAuthConfig): Promise<void> {
// Codex's streamable_http MCP support doesn't accept bearer_token (only
// bearer_token_env_var), and default_tools_approval_mode isn't valid
// under [mcp_servers.*] either - it belongs to [apps.<id>]. Both fields
// make Codex refuse to load config.toml.
// We register only url; the user runs codex mcp login custena
// afterwards to do the OAuth flow. Codex handles PKCE + token refresh.
const existing = await fs.readFile(this.configPath, 'utf-8').catch(() => '');
const patched = patchTomlSection(existing, 'mcp_servers.custena', {
url: MCP_URL,
bearer_token: oauth.accessToken,
default_tools_approval_mode: 'approve',
});
await fs.mkdir(this.configDir, { recursive: true });
await fs.writeFile(this.configPath, patched, 'utf-8');
Expand Down
13 changes: 13 additions & 0 deletions src/commands/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,5 +103,18 @@ export function installCommand(): Command {
const names = targets.map(t => t.adapter.displayName).join(', ');
console.log(chalk.bold(`\n✓ Custena Connect is ready on: ${names}`));
console.log('These agents will now pay HTTP 402 responses from your Custena account.');

const notes = targets.filter(t => t.adapter.postInstallNote);
if (notes.length > 0) {
console.log('');
for (const { adapter } of notes) {
console.log(chalk.yellow(` ${adapter.displayName}: ${adapter.postInstallNote}`));
}
}

if (targets.some(t => t.adapter.id === 'codex')) {
console.log('');
console.log(chalk.cyan('For OpenAI Codex:') + ' run ' + chalk.bold('codex mcp login custena') + ' to authenticate.');
}
});
}
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export interface HostAdapter {
id: string;
displayName: string;
capabilities: { mcpPrompts: boolean; hooks: boolean };
/** Printed after install if present - use for host-specific next steps. */
postInstallNote?: string;
detect(): Promise<HostPresence>;
writeMcpConfig(oauth: OAuthConfig): Promise<void>;
writeSkill(): Promise<void>;
Expand Down
Loading