mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-29 14:37:44 +02:00
* feat(agent): add Cursor Agent CLI runtime support Add cursor-agent as a new agent backend, following the same pattern as existing providers. The implementation spawns cursor-agent CLI with stream-json output, parses JSONL events into the unified Message type, and supports session resume, usage tracking, and auto-approval (--yolo). Changes: - server/pkg/agent/cursor.go: cursorBackend implementation - server/pkg/agent/cursor_test.go: unit tests for args, parsing, errors - server/pkg/agent/agent.go: register "cursor" in New() factory - server/internal/daemon/config.go: probe cursor-agent in PATH - server/internal/daemon/execenv/context.go: cursor skill discovery path - server/internal/daemon/execenv/runtime_config.go: AGENTS.md injection - packages/views/.../provider-logo.tsx: cursor logo in UI Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(agent): address PR review for cursor backend 1. Fix token usage double-counting: usage is now taken exclusively from "result" events (session totals). Per-message usage in "assistant" events is intentionally ignored. "step_finish" usage is only used as fallback when no "result" usage is available. 2. Remove dead code: isCursorUnknownSessionError() and its regex were defined but never called. Removed along with corresponding test. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(agent): add missing CustomArgs, SystemPrompt, MaxTurns, and debug logging to cursor backend - Add cursorBlockedArgs and filterCustomArgs support for safe custom arg passthrough - Add --system-prompt and --max-turns flag support to buildCursorArgs - Add debug logging of command args before execution (consistent with all other backends) - Move stdout-close goroutine inside main goroutine (consistent with claude.go pattern) - Add tests for SystemPrompt/MaxTurns and CustomArgs filtering Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: make daemon uses local profile & update Cursor logo to official brand - Makefile: make daemon now runs 'daemon start --profile local' for local dev - Replace Cursor runtime logo with official brand SVG (removed background rect) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(agent): remove unsupported --system-prompt and --max-turns from cursor-agent cursor-agent CLI does not support these flags. Instructions are already injected via AGENTS.md and .cursor/skills/ files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(agent): prevent step_finish + result usage double-counting in cursor Split usage accumulation into separate stepUsage and resultUsage maps. After stream ends, use resultUsage if available (session totals from result event), otherwise fall back to stepUsage (sum of step_finish). This prevents 2x counting when result.usage already includes totals. Added table-driven test covering: result-only, step_finish-only, step_finish+result (no double count), and multi-model scenarios. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(agent): fix misleading comment on cursor -p flag Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Devv <devv@Devvs-Mac-mini.local> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: yushen <ldnvnbl@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
189 lines
12 KiB
Go
189 lines
12 KiB
Go
package execenv
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// InjectRuntimeConfig writes the meta skill content into the runtime-specific
|
|
// config file so the agent discovers its environment through its native mechanism.
|
|
//
|
|
// For Claude: writes {workDir}/CLAUDE.md (skills discovered natively from .claude/skills/)
|
|
// For Codex: writes {workDir}/AGENTS.md (skills discovered natively via CODEX_HOME)
|
|
// For OpenCode: writes {workDir}/AGENTS.md (skills discovered natively from .config/opencode/skills/)
|
|
// For OpenClaw: writes {workDir}/AGENTS.md (skills discovered natively from .openclaw/skills/)
|
|
// For Gemini: writes {workDir}/GEMINI.md (discovered natively by the Gemini CLI)
|
|
// For Pi: writes {workDir}/AGENTS.md (skills discovered natively from ~/.pi/agent/skills/)
|
|
// For Cursor: writes {workDir}/AGENTS.md (skills discovered natively from .cursor/skills/)
|
|
func InjectRuntimeConfig(workDir, provider string, ctx TaskContextForEnv) error {
|
|
content := buildMetaSkillContent(provider, ctx)
|
|
|
|
switch provider {
|
|
case "claude":
|
|
return os.WriteFile(filepath.Join(workDir, "CLAUDE.md"), []byte(content), 0o644)
|
|
case "codex", "opencode", "openclaw", "pi", "cursor":
|
|
return os.WriteFile(filepath.Join(workDir, "AGENTS.md"), []byte(content), 0o644)
|
|
case "gemini":
|
|
return os.WriteFile(filepath.Join(workDir, "GEMINI.md"), []byte(content), 0o644)
|
|
default:
|
|
// Unknown provider — skip config injection, prompt-only mode.
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// buildMetaSkillContent generates the meta skill markdown that teaches the agent
|
|
// about the Multica runtime environment and available CLI tools.
|
|
func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
|
|
var b strings.Builder
|
|
|
|
b.WriteString("# Multica Agent Runtime\n\n")
|
|
b.WriteString("You are a coding agent in the Multica platform. Use the `multica` CLI to interact with the platform.\n\n")
|
|
|
|
// Always emit agent identity so the agent knows who it is, even when
|
|
// dispatched via @mention on an issue assigned to a different agent.
|
|
if ctx.AgentName != "" || ctx.AgentID != "" {
|
|
b.WriteString("## Agent Identity\n\n")
|
|
if ctx.AgentName != "" {
|
|
fmt.Fprintf(&b, "**You are: %s**", ctx.AgentName)
|
|
if ctx.AgentID != "" {
|
|
fmt.Fprintf(&b, " (ID: `%s`)", ctx.AgentID)
|
|
}
|
|
b.WriteString("\n\n")
|
|
}
|
|
if ctx.AgentInstructions != "" {
|
|
b.WriteString(ctx.AgentInstructions)
|
|
b.WriteString("\n\n")
|
|
}
|
|
} else if ctx.AgentInstructions != "" {
|
|
b.WriteString("## Agent Identity\n\n")
|
|
b.WriteString(ctx.AgentInstructions)
|
|
b.WriteString("\n\n")
|
|
}
|
|
|
|
b.WriteString("## Available Commands\n\n")
|
|
b.WriteString("**Always use `--output json` for all read commands** to get structured data with full IDs.\n\n")
|
|
b.WriteString("### Read\n")
|
|
b.WriteString("- `multica issue get <id> --output json` — Get full issue details (title, description, status, priority, assignee)\n")
|
|
b.WriteString("- `multica issue list [--status X] [--priority X] [--assignee X] [--limit N] [--offset N] --output json` — List issues in workspace (default limit: 50; JSON output includes `total`, `has_more` — use offset to paginate when `has_more` is true)\n")
|
|
b.WriteString("- `multica issue comment list <issue-id> [--limit N] [--offset N] [--since <RFC3339>] --output json` — List comments on an issue (supports pagination; includes id, parent_id for threading)\n")
|
|
b.WriteString("- `multica workspace get --output json` — Get workspace details and context\n")
|
|
b.WriteString("- `multica workspace members [workspace-id] --output json` — List workspace members (user IDs, names, roles)\n")
|
|
b.WriteString("- `multica agent list --output json` — List agents in workspace\n")
|
|
b.WriteString("- `multica repo checkout <url>` — Check out a repository into the working directory (creates a git worktree with a dedicated branch)\n")
|
|
b.WriteString("- `multica issue runs <issue-id> --output json` — List all execution runs for an issue (status, timestamps, errors)\n")
|
|
b.WriteString("- `multica issue run-messages <task-id> [--since <seq>] --output json` — List messages for a specific execution run (supports incremental fetch)\n")
|
|
b.WriteString("- `multica attachment download <id> [-o <dir>]` — Download an attachment file locally by ID\n\n")
|
|
|
|
b.WriteString("### Write\n")
|
|
b.WriteString("- `multica issue create --title \"...\" [--description \"...\"] [--priority X] [--assignee X] [--parent <issue-id>] [--status X]` — Create a new issue\n")
|
|
b.WriteString("- `multica issue assign <id> --to <name>` — Assign an issue to a member or agent by name (use --unassign to remove assignee)\n")
|
|
b.WriteString("- `multica issue comment add <issue-id> --content \"...\" [--parent <comment-id>]` — Post a comment (use --parent to reply to a specific comment)\n")
|
|
b.WriteString(" - For content with special characters (backticks, quotes), pipe via stdin: `cat <<'COMMENT' | multica issue comment add <issue-id> --content-stdin`\n")
|
|
b.WriteString("- `multica issue comment delete <comment-id>` — Delete a comment\n")
|
|
b.WriteString("- `multica issue status <id> <status>` — Update issue status (todo, in_progress, in_review, done, blocked)\n")
|
|
b.WriteString("- `multica issue update <id> [--title X] [--description X] [--priority X]` — Update issue fields\n\n")
|
|
|
|
// Inject available repositories section.
|
|
if len(ctx.Repos) > 0 {
|
|
b.WriteString("## Repositories\n\n")
|
|
b.WriteString("The following code repositories are available in this workspace.\n")
|
|
b.WriteString("Use `multica repo checkout <url>` to check out a repository into your working directory.\n\n")
|
|
b.WriteString("| URL | Description |\n")
|
|
b.WriteString("|-----|-------------|\n")
|
|
for _, repo := range ctx.Repos {
|
|
desc := repo.Description
|
|
if desc == "" {
|
|
desc = "—"
|
|
}
|
|
fmt.Fprintf(&b, "| %s | %s |\n", repo.URL, desc)
|
|
}
|
|
b.WriteString("\nThe checkout command creates a git worktree with a dedicated branch. You can check out one or more repos as needed.\n\n")
|
|
}
|
|
|
|
b.WriteString("### Workflow\n\n")
|
|
|
|
if ctx.ChatSessionID != "" {
|
|
// Chat task: interactive assistant mode
|
|
b.WriteString("**You are in chat mode.** A user is messaging you directly in a chat window.\n\n")
|
|
b.WriteString("- Respond conversationally and helpfully to the user's message\n")
|
|
b.WriteString("- You have full access to the `multica` CLI to look up issues, workspace info, members, agents, etc.\n")
|
|
b.WriteString("- If asked about issues, use `multica issue list --output json` or `multica issue get <id> --output json`\n")
|
|
b.WriteString("- If asked about the workspace, use `multica workspace get --output json`\n")
|
|
b.WriteString("- If asked to perform actions (create issues, update status, etc.), use the appropriate CLI commands\n")
|
|
b.WriteString("- If the task requires code changes, use `multica repo checkout <url>` to get the code first\n")
|
|
b.WriteString("- Keep responses concise and direct\n\n")
|
|
} else if ctx.TriggerCommentID != "" {
|
|
// Comment-triggered: focus on reading and replying
|
|
b.WriteString("**This task was triggered by a NEW comment.** Your primary job is to respond to THIS specific comment, even if you have handled similar requests before in this session.\n\n")
|
|
fmt.Fprintf(&b, "1. Run `multica issue get %s --output json` to understand the issue context\n", ctx.IssueID)
|
|
fmt.Fprintf(&b, "2. Run `multica issue comment list %s --output json` to read the conversation\n", ctx.IssueID)
|
|
b.WriteString(" - If the output is very large or truncated, use pagination: `--limit 30` to get the latest 30 comments, or `--since <timestamp>` to fetch only recent ones\n")
|
|
fmt.Fprintf(&b, "3. Find the triggering comment (ID: `%s`) and understand what is being asked — do NOT confuse it with previous comments\n", ctx.TriggerCommentID)
|
|
fmt.Fprintf(&b, "4. Reply: `multica issue comment add %s --parent %s --content \"...\"`\n", ctx.IssueID, ctx.TriggerCommentID)
|
|
b.WriteString("5. If the comment requests code changes or further work, do the work first, then reply with your results\n")
|
|
b.WriteString("6. Do NOT change the issue status unless the comment explicitly asks for it\n\n")
|
|
} else {
|
|
// Assignment-triggered: defer to agent Skills for workflow specifics.
|
|
b.WriteString("You are responsible for managing the issue status throughout your work.\n\n")
|
|
fmt.Fprintf(&b, "1. Run `multica issue get %s --output json` to understand your task\n", ctx.IssueID)
|
|
fmt.Fprintf(&b, "2. Run `multica issue status %s in_progress`\n", ctx.IssueID)
|
|
b.WriteString("3. Read comments for additional context or human instructions\n")
|
|
b.WriteString("4. Follow your Skills and Agent Identity to determine how to complete this task.\n")
|
|
b.WriteString(" If no relevant skill applies, the default workflow is: understand the task → do the work → post a comment with results → update issue status.\n")
|
|
fmt.Fprintf(&b, "5. When done, run `multica issue status %s in_review`\n", ctx.IssueID)
|
|
fmt.Fprintf(&b, "6. If blocked, run `multica issue status %s blocked` and post a comment explaining why\n\n", ctx.IssueID)
|
|
}
|
|
|
|
if len(ctx.AgentSkills) > 0 {
|
|
b.WriteString("## Skills\n\n")
|
|
switch provider {
|
|
case "claude":
|
|
// Claude discovers skills natively from .claude/skills/ — just list names.
|
|
b.WriteString("You have the following skills installed (discovered automatically):\n\n")
|
|
case "codex", "opencode", "openclaw", "pi", "cursor":
|
|
// Codex, OpenCode, OpenClaw, Pi, and Cursor discover skills natively from their respective paths — just list names.
|
|
b.WriteString("You have the following skills installed (discovered automatically):\n\n")
|
|
case "gemini":
|
|
// Gemini reads GEMINI.md directly; point it at the fallback skills dir.
|
|
b.WriteString("Detailed skill instructions are in `.agent_context/skills/`. Each subdirectory contains a `SKILL.md`.\n\n")
|
|
default:
|
|
b.WriteString("Detailed skill instructions are in `.agent_context/skills/`. Each subdirectory contains a `SKILL.md`.\n\n")
|
|
}
|
|
for _, skill := range ctx.AgentSkills {
|
|
fmt.Fprintf(&b, "- **%s**\n", skill.Name)
|
|
}
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
b.WriteString("## Mentions\n\n")
|
|
b.WriteString("When referencing issues or people in comments, use the mention format so they render as interactive links:\n\n")
|
|
b.WriteString("- **Issue**: `[MUL-123](mention://issue/<issue-id>)` — renders as a clickable link to the issue\n")
|
|
b.WriteString("- **Member**: `[@Name](mention://member/<user-id>)` — renders as a styled mention and sends a notification\n")
|
|
b.WriteString("- **Agent**: `[@Name](mention://agent/<agent-id>)` — renders as a styled mention\n\n")
|
|
b.WriteString("Use `multica issue list --output json` to look up issue IDs, and `multica workspace members --output json` for member IDs.\n\n")
|
|
|
|
b.WriteString("## Attachments\n\n")
|
|
b.WriteString("Issues and comments may include file attachments (images, documents, etc.).\n")
|
|
b.WriteString("Use the download command to fetch attachment files locally:\n\n")
|
|
b.WriteString("```\nmultica attachment download <attachment-id>\n```\n\n")
|
|
b.WriteString("This downloads the file to the current directory and prints the local path. Use `-o <dir>` to save elsewhere.\n")
|
|
b.WriteString("After downloading, you can read the file directly (e.g. view an image, read a document).\n\n")
|
|
|
|
b.WriteString("## Important: Always Use the `multica` CLI\n\n")
|
|
b.WriteString("All interactions with Multica platform resources — including issues, comments, attachments, images, files, and any other platform data — **must** go through the `multica` CLI. ")
|
|
b.WriteString("Do NOT use `curl`, `wget`, or any other HTTP client to access Multica URLs or APIs directly. ")
|
|
b.WriteString("Multica resource URLs require authenticated access that only the `multica` CLI can provide.\n\n")
|
|
b.WriteString("If you need to perform an operation that is not covered by any existing `multica` command, ")
|
|
b.WriteString("do NOT attempt to work around it. Instead, post a comment mentioning the workspace owner to request the missing functionality.\n\n")
|
|
|
|
b.WriteString("## Output\n\n")
|
|
b.WriteString("Keep comments concise and natural — state the outcome, not the process.\n")
|
|
b.WriteString("Good: \"Fixed the login redirect. PR: https://...\"\n")
|
|
b.WriteString("Bad: \"1. Read the issue 2. Found the bug in auth.go 3. Created branch 4. ...\"\n")
|
|
b.WriteString("When referencing issues in comments, **always** use the mention format `[MUL-123](mention://issue/<issue-id>)` so they render as clickable links.\n")
|
|
|
|
return b.String()
|
|
}
|