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
10 changes: 10 additions & 0 deletions .cursor/rules/commonly.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
description: Use Commonly as the shared pod for Cursor work
alwaysApply: true
---

When a task names a Commonly pod, call `commonly_get_context(podId)` before starting so you have its current priorities and recent messages. Use `commonly_get_messages` when you need more history. Post a concise result or handoff to that same pod with `commonly_post_message`; avoid step-by-step progress chatter.

Cursor's MCP connection lets this session use Commonly tools. It does not watch for mentions or wake Cursor while no session is open. Do not claim that a Cursor agent is listening for @mentions unless a separate listener has been set up and verified.

Keep agent keys out of pod messages, URLs, logs, and committed files. For work in this repository, follow [AGENTS.md](../../AGENTS.md) and [REVIEW.md](../../REVIEW.md).
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ dist/

*.pem

.cursor/
/.cursor/*
!/.cursor/rules/
/.cursor/rules/*
!/.cursor/rules/commonly.mdc

# Playwright
/test-results/
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1466,6 +1466,8 @@
"runtimeToken": "The key on its own",
"claudeCode": "Optional: let Claude Code use the pod in your own sessions",
"cursor": "Use the pod from Cursor via MCP — add to ~/.cursor/mcp.json",
"cursorInstall": "Add to Cursor",
"cursorInstallHint": "The link adds Commonly with a placeholder key. Replace it in Cursor’s MCP settings with the key above.",
"memory": "Bring its memory (optional)",
"claudeCodeBody": "Gives your own Claude Code sessions Commonly’s tools. It does not make {{name}} answer @mentions; the command above does.",
"claudeCodeBodyOwn": "Gives your own Claude Code sessions Commonly’s tools.",
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1526,6 +1526,8 @@
"runtimeToken": "仅密钥",
"claudeCode": "可选:让 Claude Code 在你自己的会话里使用这个 Pod",
"cursor": "通过 MCP 在 Cursor 中使用这个 Pod——添加到 ~/.cursor/mcp.json",
"cursorInstall": "添加到 Cursor",
"cursorInstallHint": "该链接会以占位密钥添加 Commonly。请在 Cursor 的 MCP 设置中将其替换为上面的密钥。",
"memory": "导入它的记忆(可选)",
"claudeCodeBody": "为你自己的 Claude Code 会话加上 Commonly 的工具。它不会让 {{name}} 回复 @提及,上面那条命令才会。",
"claudeCodeBodyOwn": "为你自己的 Claude Code 会话加上 Commonly 的工具。",
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/setupTests.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
// Import jest-dom matchers
import '@testing-library/jest-dom';
import { TextEncoder } from 'util';
import './i18n';

// jsdom omits this browser API; use Node's implementation for UTF-8 encoding.
Object.assign(global, { TextEncoder });

// Mock axios
jest.mock('axios', () => ({
__esModule: true,
Expand Down
28 changes: 28 additions & 0 deletions frontend/src/v2/__tests__/V2AgentBYOListenSnippet.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,34 @@ describe('BYO listener snippet', () => {
}
});

test('the Cursor install link uses this instance without putting the issued key in its URL', async () => {
const previousApiUrl = process.env.REACT_APP_API_URL;
process.env.REACT_APP_API_URL = 'https://api.example.test/';
try {
await issueToken();
const href = screen.getByRole('link', { name: 'Add to Cursor', hidden: true }).getAttribute('href') || '';
const config = JSON.parse(atob(decodeURIComponent(href.split('config=')[1])));
expect(config.env).toEqual({
COMMONLY_API_URL: 'https://api.example.test',
COMMONLY_AGENT_TOKEN: 'PASTE_AGENT_TOKEN_HERE',
});
expect(href).not.toContain('cm_agent_test_token');
expect(href).not.toContain('api.commonly.me');
} finally {
if (previousApiUrl === undefined) delete process.env.REACT_APP_API_URL;
else process.env.REACT_APP_API_URL = previousApiUrl;
}
});

test('the Cursor link leads its guidance, while the JSON still names its destination', async () => {
await issueToken();
const link = screen.getByRole('link', { name: 'Add to Cursor', hidden: true });
const hint = link.nextElementSibling;
expect(hint).toHaveClass('v2-byo__hint');
expect(hint).toHaveTextContent('The link adds Commonly with a placeholder key.');
expect(link.closest('.v2-byo__snippet')).toHaveTextContent('~/.cursor/mcp.json');
});

// QA BYO walk 1 (2026-10-16): four equal Copy boxes, and the one titled
// "Claude Code" was the MCP line, which does not make the agent answer. The
// command that does is THE box: first, and the only one not marked optional
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/v2/__tests__/V2ConnectPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ describe('V2ConnectPage', () => {
expect(mcpSnippet).toContain('COMMONLY_API_URL=https://api.example.test');
expect(cliSnippet).not.toContain('api.commonly.me');
expect(mcpSnippet).not.toContain('api.commonly.me');
const cursorHref = screen.getByRole('link', { name: 'Add Commonly to Cursor' }).getAttribute('href') || '';
expect(cursorHref).toMatch(/^cursor:\/\/anysphere\.cursor-deeplink\/mcp\/install\?name=commonly&config=/);
const cursorConfig = JSON.parse(atob(decodeURIComponent(cursorHref.split('config=')[1])));
expect(cursorConfig.env).toEqual({
COMMONLY_API_URL: 'https://api.example.test',
COMMONLY_AGENT_TOKEN: 'PASTE_AGENT_TOKEN_HERE',
});
expect(cursorHref).not.toContain('api.commonly.me');
fireEvent.click(screen.getAllByRole('button', { name: 'Copy' })[0]);
await waitFor(() => expect(clipboard.writeText).toHaveBeenCalledWith(cliSnippet));
} finally {
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/v2/__tests__/cursorMcpInstallLink.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import cursorMcpInstallLink from '../utils/cursorMcpInstallLink';

test('Cursor config preserves Unicode API URLs as UTF-8', () => {
const apiUrl = 'https://例子.test/连接/';
const href = cursorMcpInstallLink(apiUrl);
const encodedConfig = new URL(href).searchParams.get('config') || '';
const config = JSON.parse(Buffer.from(encodedConfig, 'base64').toString('utf8'));

expect(config.env).toEqual({
COMMONLY_API_URL: 'https://例子.test/连接',
COMMONLY_AGENT_TOKEN: 'PASTE_AGENT_TOKEN_HERE',
});
});
4 changes: 4 additions & 0 deletions frontend/src/v2/components/V2AgentBYO.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { DEFAULT_INSTALL_SCOPES } from '../utils/agentInstallScopes';
import { hostedCapNotice } from './hostedCapNotice';
import { readPodVisits } from '../lib/podRecency';
import { AgentConnection, connectionLabel } from '../utils/agentConnection';
import cursorMcpInstallLink from '../utils/cursorMcpInstallLink';

const DEFAULT_AGENT_NAME = 'my-mcp-agent';
// The token routes refuse a name that belongs to an agent Commonly runs (the
Expand Down Expand Up @@ -897,6 +898,7 @@ const V2AgentBYO: React.FC = () => {
},
}, null, 2)
: '';
const cursorInstallHref = cursorMcpInstallLink(apiUrl);

return (
<V2FeaturePage
Expand Down Expand Up @@ -1486,6 +1488,8 @@ const V2AgentBYO: React.FC = () => {
<div className="v2-byo__snippet-head">
<span>{t('agentByo.snippets.cursor')}</span>
</div>
<a className="v2-byo__cursor-link" href={cursorInstallHref}>{t('agentByo.snippets.cursorInstall')}</a>
<p className="v2-byo__hint">{t('agentByo.snippets.cursorInstallHint')}</p>
<div className="v2-byo__command">
<pre className="v2-byo__pre">{cursorSnippet}</pre>
<button type="button" onClick={() => copy('cursor', cursorSnippet)} className="v2-byo__copy">
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/v2/components/V2ConnectPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
import axios from '../../utils/axiosConfig';
import getApiBaseUrl from '../../utils/apiBaseUrl';
import { useAuth } from '../../context/AuthContext';
import cursorMcpInstallLink from '../utils/cursorMcpInstallLink';

type CopyKey = 'cli' | 'mcp' | 'gateway';

Expand All @@ -17,6 +18,7 @@ const CopyButton: React.FC<{ value: string; copyKey: CopyKey; copied: CopyKey |
const V2ConnectPage: React.FC = () => {
const apiBaseUrl = getApiBaseUrl().replace(/\/+$/, '');
const cliSnippet = `npm install -g @commonlyai/cli\ncommonly login --instance ${apiBaseUrl}`;
const cursorInstallHref = cursorMcpInstallLink(apiBaseUrl);
const mcpSnippet = `claude mcp add commonly \\
-e COMMONLY_API_URL=${apiBaseUrl} \\
-e COMMONLY_AGENT_TOKEN=cm_agent_… \\
Expand Down Expand Up @@ -106,6 +108,10 @@ const V2ConnectPage: React.FC = () => {
<pre>{mcpSnippet}</pre>
</div>
</div>
<div className="v2-connect__cursor">
<a className="v2-connect__link" href={cursorInstallHref}>Add Commonly to Cursor</a>
<span>Add your agent key in Cursor’s MCP settings.</span>
</div>
<Link className="v2-connect__link" to="/v2/settings">Manage connected CLI devices</Link>
</section>

Expand Down
19 changes: 19 additions & 0 deletions frontend/src/v2/utils/cursorMcpInstallLink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Cursor's install deeplink contains its MCP configuration in the URL. Keep
// agent keys out of it; the user adds the key in Cursor after installation.
export const CURSOR_TOKEN_PLACEHOLDER = 'PASTE_AGENT_TOKEN_HERE';

const cursorMcpInstallLink = (apiUrl: string): string => {
const config = {
command: 'npx',
args: ['-y', '@commonlyai/mcp'],
env: {
COMMONLY_API_URL: apiUrl.replace(/\/+$/, ''),
COMMONLY_AGENT_TOKEN: CURSOR_TOKEN_PLACEHOLDER,
},
};
const bytes = new TextEncoder().encode(JSON.stringify(config));
const encodedConfig = btoa(Array.from(bytes, (byte) => String.fromCharCode(byte)).join(''));
return `cursor://anysphere.cursor-deeplink/mcp/install?name=commonly&config=${encodeURIComponent(encodedConfig)}`;
};

export default cursorMcpInstallLink;
18 changes: 18 additions & 0 deletions frontend/src/v2/v2.css
Original file line number Diff line number Diff line change
Expand Up @@ -6481,6 +6481,13 @@ body.modern-ui.v2-canvas {
color: var(--v2-accent-text);
font: 500 13px/20px var(--v2-font);
}
.v2-root a.v2-byo__cursor-link {
align-self: flex-start;
color: var(--v2-accent-text);
font: 500 13px/20px var(--v2-font);
text-decoration: none;
}
.v2-root a.v2-byo__cursor-link:hover { text-decoration: underline; }
.v2-root button.v2-byo__secondary {
align-self: flex-start;
padding: 10px 18px;
Expand Down Expand Up @@ -6762,6 +6769,17 @@ body.modern-ui.v2-canvas {
text-decoration: underline;
}

.v2-connect__cursor {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 4px 12px;
margin-top: 12px;
color: var(--v2-text-tertiary);
font: 400 13px/20px var(--v2-font);
}
.v2-root .v2-connect__cursor .v2-connect__link { margin-top: 0; }

.v2-connect__actions {
display: flex;
flex-wrap: wrap;
Expand Down
Loading