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
38 changes: 36 additions & 2 deletions lib/agents.sh
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,12 @@ agent_add() {
echo " 1) Anthropic (Claude)"
echo " 2) OpenAI (Codex)"
echo " 3) OpenCode"
read -rp "Choose [1-3, default: 1]: " AGENT_PROVIDER_CHOICE
echo " 4) Cursor"
read -rp "Choose [1-4, default: 1]: " AGENT_PROVIDER_CHOICE
case "$AGENT_PROVIDER_CHOICE" in
2) AGENT_PROVIDER="openai" ;;
3) AGENT_PROVIDER="opencode" ;;
4) AGENT_PROVIDER="cursor" ;;
*) AGENT_PROVIDER="anthropic" ;;
esac

Expand Down Expand Up @@ -191,6 +193,21 @@ agent_add() {
8) read -rp "Enter model name (e.g. provider/model): " AGENT_MODEL ;;
*) AGENT_MODEL="opencode/claude-sonnet-4-5" ;;
esac
elif [ "$AGENT_PROVIDER" = "cursor" ]; then
echo "Model:"
echo " 1) Auto (let Cursor choose)"
echo " 2) Sonnet (claude-sonnet-4-5)"
echo " 3) Opus (claude-opus-4-6)"
echo " 4) GPT-5.2"
echo " 5) Custom (enter model name)"
read -rp "Choose [1-5, default: 1]: " AGENT_MODEL_CHOICE
case "$AGENT_MODEL_CHOICE" in
2) AGENT_MODEL="sonnet" ;;
3) AGENT_MODEL="opus" ;;
4) AGENT_MODEL="gpt-5.2" ;;
5) read -rp "Enter model name: " AGENT_MODEL ;;
*) AGENT_MODEL="auto" ;;
esac
else
echo "Model:"
echo " 1) GPT-5.3 Codex"
Expand Down Expand Up @@ -517,15 +534,32 @@ agent_provider() {
echo "Use 'tinyclaw agent provider ${agent_id} openai --model {gpt-5.3-codex|gpt-5.2}' to also set the model."
fi
;;
cursor)
if [ -n "$model_arg" ]; then
jq --arg id "$agent_id" --arg model "$model_arg" \
'.agents[$id].provider = "cursor" | .agents[$id].model = $model' \
"$SETTINGS_FILE" > "$tmp_file" && mv "$tmp_file" "$SETTINGS_FILE"
echo -e "${GREEN}✓ Agent '${agent_id}' switched to Cursor with model: ${model_arg}${NC}"
else
jq --arg id "$agent_id" \
'.agents[$id].provider = "cursor"' \
"$SETTINGS_FILE" > "$tmp_file" && mv "$tmp_file" "$SETTINGS_FILE"
echo -e "${GREEN}✓ Agent '${agent_id}' switched to Cursor${NC}"
echo ""
echo "Use 'tinyclaw agent provider ${agent_id} cursor --model {auto|sonnet|opus}' to also set the model."
fi
;;
*)
echo "Usage: tinyclaw agent provider <agent_id> {anthropic|openai} [--model MODEL_NAME]"
echo "Usage: tinyclaw agent provider <agent_id> {anthropic|openai|cursor} [--model MODEL_NAME]"
echo ""
echo "Examples:"
echo " tinyclaw agent provider coder # Show current provider/model"
echo " tinyclaw agent provider coder anthropic # Switch to Anthropic"
echo " tinyclaw agent provider coder openai # Switch to OpenAI"
echo " tinyclaw agent provider coder cursor # Switch to Cursor"
echo " tinyclaw agent provider coder anthropic --model opus # Switch to Anthropic Opus"
echo " tinyclaw agent provider coder openai --model gpt-5.3-codex # Switch to OpenAI GPT-5.3 Codex"
echo " tinyclaw agent provider coder cursor --model auto # Switch to Cursor with auto model"
exit 1
;;
esac
Expand Down
51 changes: 48 additions & 3 deletions lib/setup-wizard.sh
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,15 @@ echo ""
echo " 1) Anthropic (Claude) (recommended)"
echo " 2) OpenAI (Codex/GPT)"
echo " 3) OpenCode"
echo " 4) Cursor"
echo ""
read -rp "Choose [1-3]: " PROVIDER_CHOICE
read -rp "Choose [1-4]: " PROVIDER_CHOICE

case "$PROVIDER_CHOICE" in
1) PROVIDER="anthropic" ;;
2) PROVIDER="openai" ;;
3) PROVIDER="opencode" ;;
4) PROVIDER="cursor" ;;
*)
echo -e "${RED}Invalid choice${NC}"
exit 1
Expand Down Expand Up @@ -176,6 +178,36 @@ elif [ "$PROVIDER" = "opencode" ]; then
esac
echo -e "${GREEN}✓ Model: $MODEL${NC}"
echo ""
elif [ "$PROVIDER" = "cursor" ]; then
echo "Which Cursor model?"
echo ""
echo " 1) Auto (let Cursor choose, recommended)"
echo " 2) Sonnet (claude-sonnet-4-5)"
echo " 3) Opus (claude-opus-4-6)"
echo " 4) GPT-5.2"
echo " 5) Custom (enter model name)"
echo ""
read -rp "Choose [1-5]: " MODEL_CHOICE

case "$MODEL_CHOICE" in
1) MODEL="auto" ;;
2) MODEL="sonnet" ;;
3) MODEL="opus" ;;
4) MODEL="gpt-5.2" ;;
5)
read -rp "Enter model name: " MODEL
if [ -z "$MODEL" ]; then
echo -e "${RED}Model name required${NC}"
exit 1
fi
;;
*)
echo -e "${RED}Invalid choice${NC}"
exit 1
;;
esac
echo -e "${GREEN}✓ Model: $MODEL${NC}"
echo ""
else
# OpenAI models
echo "Which OpenAI model?"
Expand Down Expand Up @@ -288,11 +320,12 @@ if [[ "$SETUP_AGENTS" =~ ^[yY] ]]; then
read -rp " Display name: " NEW_AGENT_NAME
[ -z "$NEW_AGENT_NAME" ] && NEW_AGENT_NAME="$NEW_AGENT_ID"

echo " Provider: 1) Anthropic 2) OpenAI 3) OpenCode"
read -rp " Choose [1-3, default: 1]: " NEW_PROVIDER_CHOICE
echo " Provider: 1) Anthropic 2) OpenAI 3) OpenCode 4) Cursor"
read -rp " Choose [1-4, default: 1]: " NEW_PROVIDER_CHOICE
case "$NEW_PROVIDER_CHOICE" in
2) NEW_PROVIDER="openai" ;;
3) NEW_PROVIDER="opencode" ;;
4) NEW_PROVIDER="cursor" ;;
*) NEW_PROVIDER="anthropic" ;;
esac

Expand All @@ -314,6 +347,16 @@ if [[ "$SETUP_AGENTS" =~ ^[yY] ]]; then
5) read -rp " Enter model name (e.g. provider/model): " NEW_MODEL ;;
*) NEW_MODEL="opencode/claude-sonnet-4-5" ;;
esac
elif [ "$NEW_PROVIDER" = "cursor" ]; then
echo " Model: 1) Auto 2) Sonnet 3) Opus 4) GPT-5.2 5) Custom"
read -rp " Choose [1-5, default: 1]: " NEW_MODEL_CHOICE
case "$NEW_MODEL_CHOICE" in
2) NEW_MODEL="sonnet" ;;
3) NEW_MODEL="opus" ;;
4) NEW_MODEL="gpt-5.2" ;;
5) read -rp " Enter model name: " NEW_MODEL ;;
*) NEW_MODEL="auto" ;;
esac
else
echo " Model: 1) GPT-5.3 Codex 2) GPT-5.2 3) Custom"
read -rp " Choose [1-3, default: 1]: " NEW_MODEL_CHOICE
Expand Down Expand Up @@ -357,6 +400,8 @@ if [ "$PROVIDER" = "anthropic" ]; then
MODELS_SECTION='"models": { "provider": "anthropic", "anthropic": { "model": "'"${MODEL}"'" } }'
elif [ "$PROVIDER" = "opencode" ]; then
MODELS_SECTION='"models": { "provider": "opencode", "opencode": { "model": "'"${MODEL}"'" } }'
elif [ "$PROVIDER" = "cursor" ]; then
MODELS_SECTION='"models": { "provider": "cursor", "cursor": { "model": "'"${MODEL}"'" } }'
else
MODELS_SECTION='"models": { "provider": "openai", "openai": { "model": "'"${MODEL}"'" } }'
fi
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions src/lib/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ export function ensureAgentDirectory(agentDir: string): void {
copyDirSync(targetAgentsSkills, targetClaudeSkills);
}

// Copy AGENTS.md as .cursor/rules/agents.mdc for Cursor CLI
if (fs.existsSync(sourceAgents)) {
const targetCursorRules = path.join(agentDir, '.cursor', 'rules');
fs.mkdirSync(targetCursorRules, { recursive: true });
fs.copyFileSync(sourceAgents, path.join(targetCursorRules, 'agents.mdc'));
}

// Create .tinyclaw directory and copy SOUL.md
const targetTinyclaw = path.join(agentDir, '.tinyclaw');
fs.mkdirSync(targetTinyclaw, { recursive: true });
Expand Down Expand Up @@ -144,4 +151,19 @@ export function updateAgentTeammates(agentDir: string, agentId: string, agents:
claudeContent = claudeContent.trimEnd() + '\n\n' + startMarker + block + endMarker + '\n';
}
fs.writeFileSync(claudeMdPath, claudeContent);

// Also write to .cursor/rules/agents.mdc for Cursor CLI
const cursorRulesDir = path.join(agentDir, '.cursor', 'rules');
const cursorRulePath = path.join(cursorRulesDir, 'agents.mdc');
if (fs.existsSync(cursorRulePath)) {
let cursorContent = fs.readFileSync(cursorRulePath, 'utf8');
const crStartIdx = cursorContent.indexOf(startMarker);
const crEndIdx = cursorContent.indexOf(endMarker);
if (crStartIdx !== -1 && crEndIdx !== -1) {
cursorContent = cursorContent.substring(0, crStartIdx + startMarker.length) + block + cursorContent.substring(crEndIdx);
} else {
cursorContent = cursorContent.trimEnd() + '\n\n' + startMarker + block + endMarker + '\n';
}
fs.writeFileSync(cursorRulePath, cursorContent);
}
}
15 changes: 14 additions & 1 deletion src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from 'fs';
import path from 'path';
import { jsonrepair } from 'jsonrepair';
import { Settings, AgentConfig, TeamConfig, CLAUDE_MODEL_IDS, CODEX_MODEL_IDS, OPENCODE_MODEL_IDS } from './types';
import { Settings, AgentConfig, TeamConfig, CLAUDE_MODEL_IDS, CODEX_MODEL_IDS, OPENCODE_MODEL_IDS, CURSOR_MODEL_IDS } from './types';

export const SCRIPT_DIR = path.resolve(__dirname, '../..');
const _localTinyclaw = path.join(SCRIPT_DIR, '.tinyclaw');
Expand Down Expand Up @@ -45,6 +45,9 @@ export function getSettings(): Settings {
if (settings?.models?.openai) {
if (!settings.models) settings.models = {};
settings.models.provider = 'openai';
} else if (settings?.models?.cursor) {
if (!settings.models) settings.models = {};
settings.models.provider = 'cursor';
} else if (settings?.models?.opencode) {
if (!settings.models) settings.models = {};
settings.models.provider = 'opencode';
Expand All @@ -71,6 +74,8 @@ export function getDefaultAgentFromModels(settings: Settings): AgentConfig {
model = settings?.models?.openai?.model || 'gpt-5.3-codex';
} else if (provider === 'opencode') {
model = settings?.models?.opencode?.model || 'sonnet';
} else if (provider === 'cursor') {
model = settings?.models?.cursor?.model || 'auto';
} else {
model = settings?.models?.anthropic?.model || 'sonnet';
}
Expand Down Expand Up @@ -127,3 +132,11 @@ export function resolveCodexModel(model: string): string {
export function resolveOpenCodeModel(model: string): string {
return OPENCODE_MODEL_IDS[model] || model || '';
}

/**
* Resolve the model ID for Cursor CLI (passed via --model flag).
* Falls back to the raw model string from settings if no mapping is found.
*/
export function resolveCursorModel(model: string): string {
return CURSOR_MODEL_IDS[model] || model || '';
}
55 changes: 54 additions & 1 deletion src/lib/invoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { spawn } from 'child_process';
import fs from 'fs';
import path from 'path';
import { AgentConfig, TeamConfig } from './types';
import { SCRIPT_DIR, resolveClaudeModel, resolveCodexModel, resolveOpenCodeModel } from './config';
import { SCRIPT_DIR, resolveClaudeModel, resolveCodexModel, resolveOpenCodeModel, resolveCursorModel } from './config';
import { log } from './logging';
import { ensureAgentDirectory, updateAgentTeammates } from './agent';

Expand Down Expand Up @@ -156,6 +156,59 @@ export async function invokeAgent(
}

return response || 'Sorry, I could not generate a response from OpenCode.';
} else if (provider === 'cursor') {
// Cursor CLI — non-interactive mode via `agent -p`.
// Uses --output-format json for structured output; single JSON object with .result field.
// Supports --continue for session continuation and --force for auto-approving tool calls.
const modelId = resolveCursorModel(agent.model);
log('INFO', `Using Cursor CLI (agent: ${agentId}, model: ${modelId || 'auto'})`);

const continueConversation = !shouldReset;

if (shouldReset) {
log('INFO', `🔄 Resetting Cursor conversation for agent: ${agentId}`);
}

const buildCursorArgs = (withContinue: boolean) => {
const args = ['-p', message, '--output-format', 'json', '--force'];
if (modelId) {
args.push('--model', modelId);
}
if (withContinue) {
args.push('--continue');
}
args.push('--workspace', workingDir);
return args;
};

const parseCursorOutput = (output: string): string | null => {
try {
const json = JSON.parse(output.trim());
if (json.type === 'result' && json.subtype === 'success' && json.result) {
return json.result;
}
} catch (e) {
if (output.trim()) {
return output.trim();
}
}
return null;
};

let cursorOutput: string;
try {
cursorOutput = await runCommand('agent', buildCursorArgs(continueConversation), workingDir);
} catch (err: any) {
// --continue fails when no previous session exists; retry without it
if (continueConversation && err?.message?.includes('No previous chats found')) {
log('INFO', `No previous Cursor session for agent ${agentId}, starting fresh`);
cursorOutput = await runCommand('agent', buildCursorArgs(false), workingDir);
} else {
throw err;
}
}

return parseCursorOutput(cursorOutput) || 'Sorry, I could not generate a response from Cursor.';
} else {
// Default to Claude (Anthropic)
log('INFO', `Using Claude provider (agent: ${agentId})`);
Expand Down
18 changes: 16 additions & 2 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export interface AgentConfig {
name: string;
provider: string; // 'anthropic', 'openai', or 'opencode'
provider: string; // 'anthropic', 'openai', 'opencode', or 'cursor'
model: string; // e.g. 'sonnet', 'opus', 'gpt-5.3-codex'
working_directory: string;
system_prompt?: string;
Expand Down Expand Up @@ -43,7 +43,7 @@ export interface Settings {
whatsapp?: {};
};
models?: {
provider?: string; // 'anthropic', 'openai', or 'opencode'
provider?: string; // 'anthropic', 'openai', 'opencode', or 'cursor'
anthropic?: {
model?: string;
};
Expand All @@ -53,6 +53,9 @@ export interface Settings {
opencode?: {
model?: string;
};
cursor?: {
model?: string;
};
};
agents?: Record<string, AgentConfig>;
teams?: Record<string, TeamConfig>;
Expand Down Expand Up @@ -140,3 +143,14 @@ export const OPENCODE_MODEL_IDS: Record<string, string> = {
'sonnet': 'opencode/claude-sonnet-4-5',
'opus': 'opencode/claude-opus-4-6',
};

// Cursor CLI model IDs. Available models depend on the user's subscription;
// run `agent models` to list them. Falls back to the raw model string.
export const CURSOR_MODEL_IDS: Record<string, string> = {
'sonnet': 'claude-sonnet-4-5',
'opus': 'claude-opus-4-6',
'claude-sonnet-4-5': 'claude-sonnet-4-5',
'claude-opus-4-6': 'claude-opus-4-6',
'gpt-5.2': 'gpt-5.2',
'auto': 'auto',
};
Loading