Skill: zhangzeyu99-web/openclaw-cursor-agent (version 1.0.0)

--- name: openclaw-cursor-agent description: Manage long-running Cursor CLI coding tasks through OpenClaw tools backed by tmux sessions. Use when the user asks to start a persistent coding job, inspect task status, send follow-up instructions, stop a running Cursor session, or mentions Cursor 后台任务, /cursor, tmux, spawn, check-status, send-command, or kill-session. --- # OpenClaw Cursor Agent 让 OpenClaw 把长时间编码任务交给 Cursor CLI 在 tmux 里持久运行。 ## 架构 ```text 用户（飞书/聊天） → OpenClaw Agent → 插件 → WSL → tmux → Cursor CLI Agent ``` ## 可用工具 | 工具 | 说明 | |------|------| | `cursor_agent_spawn_task` | 启动后台 Cursor 编码任务 | | `cursor_agent_list_tasks` | 列出所有任务 | | `cursor_agent_check_status` | 查询任务状态和进度 | | `cursor_agent_send_command` | 向任务发送补充指令 | | `cursor_agent_kill_session` | 结束任务 | | `cursor_agent_doctor` | 诊断环境和依赖 | ## 工作流程 1. 环境不确定时先 `cursor_agent_doctor` 检查 2. `cursor_agent_spawn_task` 启动后台任务 3. `cursor_agent_check_status` 或 `cursor_agent_list_tasks` 监控进度 4. `cursor_agent_send_command` 发送补充指令（`/pause`、`/resume`、`/status`、或自然语言） 5. `cursor_agent_kill_session` 结束任务 ## 自然语言映射 | 用户说 | 调用工具 | |--------|----------| | 帮我用 Cursor 在后台做一个任务：… | `cursor_agent_spawn_task` | | 看看刚才那个任务做到哪了 | `cursor_agent_check_status` | | 告诉刚才那个任务：改成 RS256 | `cursor_agent_send_command` | | 把刚才那个 Cursor 任务停掉 | `cursor_agent_kill_session` | | 列出所有后台任务 | `cursor_agent_list_tasks` | | 检查 Cursor 环境是否正常 | `cursor_agent_doctor` | ## 输出规范 - spawn 后告知会话名和任务 ID - 查状态时报告当前步骤、进度百分比、tmux 是否存活 - 环境缺依赖时明确指出缺少什么（tmux / python3 / agent / WSL） ## 参考文件 - [插件入口](extensions/openclaw-cursor-agent/index.js) — 工具注册和执行逻辑 - [工具脚本](cursor-agent-system/scripts/) — spawn / check-status / send / kill / attach - [使用指南](docs/usage-guide.md) — 用户自然语言操作说明 - [配置示例](extensions/openclaw-cursor-agent/examples/) — openclaw.json 配置参考

path: .gitattributes
content: *.sh text eol=lf
*.md text eol=lf


path: .gitignore
content: # Local environment and large files
backup-extracted/
*.tar.gz
*.sha256
*.exe
*.msi
*.appx
node-install.log
tmp-msys2-packages/

# Sensitive local config
openclaw.json

# Runtime artifacts
cursor-agent-system/status/*.json
cursor-agent-system/tasks/*.md
cursor-agent-system/tasks/*.runner.sh
cursor-agent-system/logs/*.log

# Test artifacts (generated by smoke tests)
connectivity-test.md
wsl-test-result.txt
plugin-e2e-test.txt
feishu-health-check*.md

# Common generated folders
**/node_modules/
**/.DS_Store


path: README.md
content: # OpenClaw Cursor Agent Skill

让 OpenClaw 把长时间编码任务交给 Cursor CLI 在 tmux 里持久运行。

## 架构

```text
用户（飞书/聊天） → OpenClaw Agent → 插件 → WSL → tmux → Cursor CLI Agent
```

## 组件

| 组件 | 路径 | 说明 |
|------|------|------|
| **插件** | `extensions/openclaw-cursor-agent/` | OpenClaw 插件，注册工具和命令 |
| **工具脚本** | `cursor-agent-system/scripts/` | 5 个 Bash 脚本管理 tmux 会话 |
| **Cursor Skill** | `.cursor/skills/openclaw-cursor-agent-system/` | 教 Cursor IDE 如何维护工具脚本 |
| **安装脚本** | `install-openclaw-cursor-agent.ps1` | Windows PowerShell 一键安装 |

## 插件工具

| 工具 | 说明 |
|------|------|
| `cursor_agent_spawn_task` | 启动后台 Cursor 编码任务 |
| `cursor_agent_list_tasks` | 列出所有任务 |
| `cursor_agent_check_status` | 查询任务状态和进度 |
| `cursor_agent_send_command` | 向任务发送补充指令 |
| `cursor_agent_kill_session` | 结束任务 |
| `cursor_agent_doctor` | 诊断环境和依赖 |

## 环境要求

- Windows 10+ 且已启用 WSL
- WSL 内安装 Debian（或其他发行版）
- WSL 内需要：`bash`、`tmux`、`python3`、Cursor CLI（`agent`）
- OpenClaw 已安装并运行

## 安装

```powershell
.\install-openclaw-cursor-agent.ps1 -DefaultProjectPath "D:\project\your-project" -WslDistro "Debian"
```

安装后验证：

```
openclaw cursor-agent-doctor
```

## 使用

直接用自然语言对 OpenClaw 说：

- **启动任务**：帮我用 Cursor 在后台做一个任务：实现用户登录接口
- **查看进度**：看看刚才那个 Cursor 任务做到哪了
- **补充要求**：告诉刚才那个任务：JWT 改成 RS256
- **结束任务**：把刚才那个 Cursor 任务停掉

## 项目结构

```text
openclaw/
├── extensions/openclaw-cursor-agent/   # OpenClaw 插件
│   ├── index.js                        # 插件入口
│   ├── openclaw.plugin.json            # 插件清单
│   ├── skill/                          # 插件 Skill（给 OpenClaw 用）
│   └── examples/                       # 配置示例
├── cursor-agent-system/                # 工具脚本
│   ├── scripts/                        # spawn / check-status / send / kill / attach
│   ├── templates/                      # 任务提示模板
│   ├── status/                         # 运行时状态 JSON
│   ├── tasks/                          # 运行时任务文件
│   └── logs/                           # 运行时日志
├── .cursor/skills/                     # Cursor IDE Skill（开发维护用）
├── docs/                               # 文档
│   ├── usage-guide.md                  # 使用指南
│   ├── archive/                        # 历史文档归档
│   └── LOCAL_SETUP_GUIDE.md            # OpenClaw 本地部署指南
├── install-openclaw-cursor-agent.ps1   # 安装脚本
├── .gitattributes                      # 强制 LF 换行
└── .gitignore
```

## 文档

- [使用指南](docs/usage-guide.md) — 自然语言操作说明
- [插件 README](extensions/openclaw-cursor-agent/README.md) — 插件详细说明
- [工具脚本 README](cursor-agent-system/README.md) — 脚本详细说明

## License

MIT


path: extensions/openclaw-cursor-agent/index.js
content: import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { spawn } from "node:child_process";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const PLUGIN_ID = "openclaw-cursor-agent";

const DEFAULTS = {
  toolkitRoot: "",
  defaultProjectPath: "",
  executionMode: process.platform === "win32" ? "wsl" : "direct",
  timeoutMs: 120000,
  shell: {
    executable: process.platform === "win32" ? "wsl.exe" : "bash",
    args: [],
    workingDirectory: "",
    wslDistro: "",
  },
};

const SPAWN_SCHEMA = {
  type: "object",
  required: ["taskName", "taskDescription"],
  properties: {
    taskName: { type: "string", description: "Short task label, for example feature-auth" },
    taskDescription: { type: "string", description: "The prompt that Cursor CLI should execute" },
    projectPath: { type: "string", description: "Optional project directory" },
    priority: { type: "string", description: "Task priority, for example low, normal, high" },
    eta: { type: "string", description: "Estimated duration, for example 45分钟" },
    taskFile: { type: "string", description: "Optional existing markdown task file" },
  },
};

const SESSION_SCHEMA = {
  type: "object",
  required: ["sessionQuery"],
  properties: {
    sessionQuery: { type: "string", description: "Full session name or task alias" },
  },
};

const SEND_SCHEMA = {
  type: "object",
  required: ["sessionQuery", "command"],
  properties: {
    sessionQuery: { type: "string", description: "Full session name or task alias" },
    command: { type: "string", description: "Instruction to send to the tmux session" },
  },
};

const KILL_SCHEMA = {
  type: "object",
  required: ["sessionQuery"],
  properties: {
    sessionQuery: { type: "string", description: "Full session name or task alias" },
    force: { type: "boolean", description: "Whether to force kill the session if graceful stop fails" },
    purge: { type: "boolean", description: "Whether to delete task, status, and log files after stop" },
  },
};

function asString(value, fallback = "") {
  return typeof value === "string" ? value : fallback;
}

function normalizeConfig(rawConfig) {
  const raw = rawConfig && typeof rawConfig === "object" ? rawConfig : {};
  const shell = raw.shell && typeof raw.shell === "object" ? raw.shell : {};
  const toolkitRootRaw = asString(raw.toolkitRoot || "").trim();
  const defaultProjectPathRaw = asString(raw.defaultProjectPath || "").trim();
  return {
    toolkitRoot: toolkitRootRaw ? path.resolve(toolkitRootRaw) : "",
    defaultProjectPath: defaultProjectPathRaw ? path.resolve(defaultProjectPathRaw) : "",
    executionMode: asString(raw.executionMode || DEFAULTS.executionMode || "direct").toLowerCase() === "wsl" ? "wsl" : "direct",
    timeoutMs: Number(raw.timeoutMs) > 0 ? Number(raw.timeoutMs) : DEFAULTS.timeoutMs,
    shell: {
      executable: asString(shell.executable || DEFAULTS.shell.executable),
      args: Array.isArray(shell.args) ? shell.args.map((x) => String(x)) : DEFAULTS.shell.args.slice(),
      workingDirectory: asString(shell.workingDirectory || DEFAULTS.shell.workingDirectory || ""),
      wslDistro: asString(shell.wslDistro || DEFAULTS.shell.wslDistro || ""),
    },
  };
}

function jsonResult(result, isError = false) {
  return {
    content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
    details: result,
    isError,
  };
}

function textResult(text, details = {}, isError = false) {
  return {
    content: [{ type: "text", text }],
    details,
    isError,
  };
}

function ensureToolkitRoot(config) {
  if (!config.toolkitRoot) {
    throw new Error("未配置 toolkitRoot，请在 openclaw.json 中设置插件的 toolkitRoot。");
  }
  const scriptsDir = path.join(config.toolkitRoot, "scripts");
  if (!fs.existsSync(scriptsDir)) {
    throw new Error(`toolkitRoot 无效，找不到脚本目录: ${scriptsDir}`);
  }
}

function resolveScriptPath(config, scriptName) {
  ensureToolkitRoot(config);
  const scriptPath = path.join(config.toolkitRoot, "scripts", scriptName);
  if (!fs.existsSync(scriptPath)) {
    throw new Error(`脚本不存在: ${scriptPath}`);
  }
  return scriptPath;
}

function normalizeFsPath(value) {
  const text = asString(value).trim();
  if (!text) return "";
  return path.resolve(text);
}

function toWslPath(input) {
  const resolved = normalizeFsPath(input);
  const normalized = resolved.replace(/\\/g, "/");
  const match = normalized.match(/^([A-Za-z]):\/(.*)$/);
  if (!match) return normalized;
  return `/mnt/${match[1].toLowerCase()}/${match[2]}`;
}

function posixQuote(value) {
  const text = String(value);
  return `'${text.replace(/'/g, `'\\''`)}'`;
}

function parseJsonOutput(stdout) {
  const text = String(stdout || "").trim();
  if (!text) return null;
  try {
    return JSON.parse(text);
  } catch (_) {
    const lines = text.split(/\r?\n/).filter(Boolean);
    for (let i = lines.length - 1; i >= 0; i -= 1) {
      try {
        return JSON.parse(lines[i]);
      } catch (_) {
        continue;
      }
    }
  }
  return null;
}

async function runShellProbe(config, commandText) {
  const cwd = config.shell.workingDirectory || config.toolkitRoot || __dirname;
  const shellExecutable = config.shell.executable || (config.executionMode === "wsl" ? "wsl.exe" : "bash");
  const shellArgs = Array.isArray(config.shell.args) ? config.shell.args.slice() : [];
  let args = [];

  if (config.executionMode === "wsl") {
    args = shellArgs.slice();
    if (config.shell.wslDistro) {
      args.push("-d", config.shell.wslDistro);
    }
    args.push("--", "bash", "-c", commandText);
  } else {
    args = [...shellArgs, "-c", commandText];
  }

  return await new Promise((resolve) => {
    const child = spawn(shellExecutable, args, {
      cwd,
      env: process.env,
      windowsHide: true,
    });

    let stdout = "";
    let stderr = "";
    let finished = false;
    const timer = setTimeout(() => {
      if (finished) return;
      finished = true;
      child.kill();
      resolve({ ok: false, code: -1, stdout, stderr: `${stderr}\nprobe timeout`.trim() });
    }, Math.min(config.timeoutMs, 15000));

    child.stdout.on("data", (chunk) => {
      stdout += chunk.toString();
    });
    child.stderr.on("data", (chunk) => {
      stderr += chunk.toString();
    });
    child.on("error", (error) => {
      if (finished) return;
      finished = true;
      clearTimeout(timer);
      resolve({ ok: false, code: -1, stdout, stderr: String(error) });
    });
    child.on("close", (code) => {
      if (finished) return;
      finished = true;
      clearTimeout(timer);
      resolve({
        ok: Number(code ?? 1) === 0,
        code: Number(code ?? 1),
        stdout,
        stderr,
      });
    });
  });
}

function summarizeProbe(result) {
  const stdout = asString(result?.stdout).trim();
  const stderr = asString(result?.stderr).trim();
  return stdout || stderr || "";
}

function boolAnd(items) {
  return items.every(Boolean);
}

function buildExecution(config, scriptName, args) {
  const scriptPath = resolveScriptPath(config, scriptName);
  const cwd = config.shell.workingDirectory || config.toolkitRoot || __dirname;

  if (config.executionMode === "wsl") {
    const shellExecutable = config.shell.executable || "wsl.exe";
    const shellArgs = Array.isArray(config.shell.args) ? config.shell.args.slice() : [];
    if (config.shell.wslDistro) {
      shellArgs.push("-d", config.shell.wslDistro);
    }
    shellArgs.push("--", "bash", toWslPath(scriptPath), ...args);
    return { executable: shellExecutable, args: shellArgs, cwd };
  }

  return {
    executable: config.shell.executable || "bash",
    args: [...(Array.isArray(config.shell.args) ? config.shell.args : []), scriptPath, ...args],
    cwd,
  };
}

async function runToolkitScript(config, scriptName, args) {
  const spec = buildExecution(config, scriptName, args);
  return await new Promise((resolve, reject) => {
    const child = spawn(spec.executable, spec.args, {
      cwd: spec.cwd,
      env: process.env,
      windowsHide: true,
    });

    let stdout = "";
    let stderr = "";
    let finished = false;

    const timeout = setTimeout(() => {
      if (finished) return;
      finished = true;
      child.kill();
      reject(new Error(`执行超时: ${scriptName}`));
    }, config.timeoutMs);

    child.stdout.on("data", (chunk) => {
      stdout += chunk.toString();
    });

    child.stderr.on("data", (chunk) => {
      stderr += chunk.toString();
    });

    child.on("error", (error) => {
      if (finished) return;
      finished = true;
      clearTimeout(timeout);
      reject(error);
    });

    child.on("close", (code) => {
      if (finished) return;
      finished = true;
      clearTimeout(timeout);
      resolve({
        code: Number(code ?? 0),
        stdout,
        stderr,
        json: parseJsonOutput(stdout),
      });
    });
  });
}

async function diagnose(config) {
  const toolkitRoot = config.toolkitRoot || "(未配置)";
  const shellExecutable = config.shell.executable || "(未配置)";
  const scriptsDir = config.toolkitRoot ? path.join(config.toolkitRoot, "scripts") : "";
  const statusDir = config.toolkitRoot ? path.join(config.toolkitRoot, "status") : "";
  const tasksDir = config.toolkitRoot ? path.join(config.toolkitRoot, "tasks") : "";
  const logsDir = config.toolkitRoot ? path.join(config.toolkitRoot, "logs") : "";
  const scriptChecks = {
    spawn: config.toolkitRoot ? fs.existsSync(path.join(scriptsDir, "spawn-cursor.sh")) : false,
    checkStatus: config.toolkitRoot ? fs.existsSync(path.join(scriptsDir, "check-status.sh")) : false,
    attachSession: config.toolkitRoot ? fs.existsSync(path.join(scriptsDir, "attach-session.sh")) : false,
    sendCommand: config.toolkitRoot ? fs.existsSync(path.join(scriptsDir, "send-command.sh")) : false,
    killSession: config.toolkitRoot ? fs.existsSync(path.join(scriptsDir, "kill-session.sh")) : false,
    common: config.toolkitRoot ? fs.existsSync(path.join(scriptsDir, "common.sh")) : false,
  };
  const scriptExists = boolAnd(Object.values(scriptChecks));
  const runtimeDirs = {
    status: config.toolkitRoot ? fs.existsSync(statusDir) : false,
    tasks: config.toolkitRoot ? fs.existsSync(tasksDir) : false,
    logs: config.toolkitRoot ? fs.existsSync(logsDir) : false,
  };
  const statusDirExists = runtimeDirs.status;
  const executableExists = shellExecutable ? (
    fs.existsSync(shellExecutable) || shellExecutable === "bash" || shellExecutable === "wsl.exe"
  ) : false;

  const shellRunner = executableExists
    ? await runShellProbe(config, "printf 'shell_ok'")
    : { ok: false, code: -1, stdout: "", stderr: "shell executable not found" };
  const pythonProbe = shellRunner.ok
    ? await runShellProbe(config, "command -v python3 && python3 --version")
    : { ok: false, code: -1, stdout: "", stderr: "shell unavailable" };
  const tmuxProbe = shellRunner.ok
    ? await runShellProbe(config, "command -v tmux && tmux -V")
    : { ok: false, code: -1, stdout: "", stderr: "shell unavailable" };
  const agentProbe = shellRunner.ok
    ? await runShellProbe(config, "if command -v agent >/dev/null 2>&1; then command -v agent && agent --version; elif command -v cursor-agent >/dev/null 2>&1; then command -v cursor-agent && cursor-agent --version; else exit 1; fi")
    : { ok: false, code: -1, stdout: "", stderr: "shell unavailable" };

  const dependencyChecks = {
    python3: {
      ok: pythonProbe.ok,
      detail: summarizeProbe(pythonProbe),
      code: pythonProbe.code,
    },
    tmux: {
      ok: tmuxProbe.ok,
      detail: summarizeProbe(tmuxProbe),
      code: tmuxProbe.code,
    },
    agent: {
      ok: agentProbe.ok,
      detail: summarizeProbe(agentProbe),
      code: agentProbe.code,
    },
  };

  const issues = [];
  if (!config.toolkitRoot) issues.push("未配置 toolkitRoot");
  if (!scriptExists) issues.push("工具脚本不完整");
  if (!runtimeDirs.status || !runtimeDirs.tasks || !runtimeDirs.logs) issues.push("运行目录未初始化");
  if (!executableExists) issues.push("shell.executable 不存在");
  if (!shellRunner.ok) issues.push("shell 运行器不可执行");
  if (!dependencyChecks.python3.ok) issues.push("缺少 python3");
  if (!dependencyChecks.tmux.ok) issues.push("缺少 tmux");
  if (!dependencyChecks.agent.ok) issues.push("缺少 agent 或 cursor-agent");

  return {
    ok: Boolean(
      config.toolkitRoot &&
      scriptExists &&
      runtimeDirs.status &&
      runtimeDirs.tasks &&
      runtimeDirs.logs &&
      executableExists &&
      shellRunner.ok &&
      dependencyChecks.python3.ok &&
      dependencyChecks.tmux.ok &&
      dependencyChecks.agent.ok
    ),
    pluginId: PLUGIN_ID,
    toolkitRoot,
    defaultProjectPath: config.defaultProjectPath || "",
    executionMode: config.executionMode,
    shellExecutable,
    shellArgs: config.shell.args,
    scriptExists,
    scriptsDir,
    scriptChecks,
    statusDirExists,
    runtimeDirs,
    executableExists,
    shellRunner: {
      ok: shellRunner.ok,
      code: shellRunner.code,
      detail: summarizeProbe(shellRunner) || "shell_ok",
    },
    dependencyChecks,
    issues,
  };
}

function summarizeSpawn(result) {
  const details = result?.json || {};
  return [
    "已提交 Cursor 后台任务。",
    `会话名: ${details.sessionName || "(未知)"}`,
    `状态: ${details.status || "(未知)"}`,
    `任务文件: ${details.taskFile || "(未知)"}`,
  ].join("\n");
}

function summarizeStatus(statusData) {
  const data = statusData?.status || {};
  const progress = data?.progress || {};
  return [
    `会话名: ${data.sessionName || statusData?.sessionName || "(未知)"}`,
    `状态: ${data.status || "(未知)"}`,
    `当前步骤: ${progress.currentStep || "(无)"}`,
    `进度估计: ${progress.estimatedProgress || "(无)"}`,
    `tmux 存活: ${statusData?.hasSession ? "是" : "否"}`,
  ].join("\n");
}

function summarizeSend(result) {
  const details = result?.json || {};
  const status = details?.status || {};
  return [
    `会话名: ${details.sessionName || "(未知)"}`,
    `是否送达: ${details.delivered ? "是" : "待确认"}`,
    `当前状态: ${status.status || "(未知)"}`,
    `当前步骤: ${status.progress?.currentStep || "(无)"}`,
  ].join("\n");
}

function summarizeKill(result) {
  const details = result?.json || {};
  return [
    `会话名: ${details.sessionName || "(未知)"}`,
    `最终状态: ${details.status || "(未知)"}`,
    `已清理文件: ${details.purged ? "是" : "否"}`,
  ].join("\n");
}

async function handleSpawnTool(config, params) {
  if (!params?.taskName || !params?.taskDescription) {
    return textResult("缺少 taskName 或 taskDescription。", {}, true);
  }
  const projectPath = normalizeFsPath(params.projectPath || config.defaultProjectPath || process.cwd());
  const args = [
    String(params.taskName),
    String(params.taskDescription),
    config.executionMode === "wsl" ? toWslPath(projectPath) : projectPath,
    "--json",
  ];
  if (params.priority) {
    args.push("--priority", String(params.priority));
  }
  if (params.eta) {
    args.push("--eta", String(params.eta));
  }
  if (params.taskFile) {
    const taskFilePath = normalizeFsPath(params.taskFile);
    args.push("--task-file", config.executionMode === "wsl" ? toWslPath(taskFilePath) : taskFilePath);
  }
  const result = await runToolkitScript(config, "spawn-cursor.sh", args);
  if (result.code !== 0 || !result.json) {
    return textResult(`启动失败。\nSTDOUT:\n${result.stdout}\nSTDERR:\n${result.stderr}`, { code: result.code }, true);
  }
  return textResult(summarizeSpawn(result), result.json, false);
}

async function handleListTool(config) {
  const result = await runToolkitScript(config, "check-status.sh", ["--json"]);
  if (result.code !== 0 || !result.json) {
    return textResult(`列出任务失败。\nSTDOUT:\n${result.stdout}\nSTDERR:\n${result.stderr}`, { code: result.code }, true);
  }
  const items = Array.isArray(result.json.items) ? result.json.items : [];
  const lines = items.length
    ? items.map((item) => `${item.sessionName} | ${item.status} | ${item.taskName || ""}`)
    : ["当前没有已记录任务。"];
  return textResult(lines.join("\n"), result.json, false);
}

async function handleStatusTool(config, sessionQuery) {
  if (!asString(sessionQuery).trim()) {
    return textResult("缺少 sessionQuery。", {}, true);
  }
  const result = await runToolkitScript(config, "check-status.sh", [String(sessionQuery), "--json"]);
  if (result.code !== 0 || !result.json) {
    return textResult(`查询状态失败。\nSTDOUT:\n${result.stdout}\nSTDERR:\n${result.stderr}`, { code: result.code }, true);
  }
  return textResult(summarizeStatus(result.json), result.json, false);
}

async function handleSendTool(config, params) {
  if (!params?.sessionQuery || !params?.command) {
    return textResult("缺少 sessionQuery 或 command。", {}, true);
  }
  const result = await runToolkitScript(config, "send-command.sh", [String(params.sessionQuery), String(params.command), "--json"]);
  if (result.code !== 0 || !result.json) {
    return textResult(`发送指令失败。\nSTDOUT:\n${result.stdout}\nSTDERR:\n${result.stderr}`, { code: result.code }, true);
  }
  return textResult(summarizeSend(result), result.json, !result.json.ok);
}

async function handleKillTool(config, params) {
  if (!params?.sessionQuery) {
    return textResult("缺少 sessionQuery。", {}, true);
  }
  const args = [String(params.sessionQuery), "--yes", "--json"];
  if (params.force) {
    args.push("--force");
  }
  if (params.purge) {
    args.push("--purge");
  }
  const result = await runToolkitScript(config, "kill-session.sh", args);
  if (result.code !== 0 && !result.json) {
    return textResult(`结束会话失败。\nSTDOUT:\n${result.stdout}\nSTDERR:\n${result.stderr}`, { code: result.code }, true);
  }
  return textResult(summarizeKill(result), result.json || { code: result.code }, result.code !== 0);
}

function registerTools(api, config) {
  api.registerTool({
    name: "cursor_agent_spawn_task",
    label: "Cursor Agent Spawn Task",
    description: "Start a long-running Cursor CLI coding task inside a tmux session through the cursor-agent-system toolkit.",
    parameters: SPAWN_SCHEMA,
    async execute(_toolCallId, params) {
      try {
        return await handleSpawnTool(config, params || {});
      } catch (error) {
        return textResult(`启动任务失败: ${error instanceof Error ? error.message : String(error)}`, {}, true);
      }
    },
  });

  api.registerTool({
    name: "cursor_agent_list_tasks",
    label: "Cursor Agent List Tasks",
    description: "List recorded Cursor task sessions managed by the cursor-agent-system toolkit.",
    parameters: { type: "object", properties: {} },
    async execute() {
      try {
        return await handleListTool(config);
      } catch (error) {
        return textResult(`列出任务失败: ${error instanceof Error ? error.message : String(error)}`, {}, true);
      }
    },
  });

  api.registerTool({
    name: "cursor_agent_check_status",
    label: "Cursor Agent Check Status",
    description: "Check task status, recent output, and tmux session information for a Cursor background task.",
    parameters: SESSION_SCHEMA,
    async execute(_toolCallId, params) {
      try {
        return await handleStatusTool(config, params?.sessionQuery);
      } catch (error) {
        return textResult(`查询状态失败: ${error instanceof Error ? error.message : String(error)}`, {}, true);
      }
    },
  });

  api.registerTool({
    name: "cursor_agent_send_command",
    label: "Cursor Agent Send Command",
    description: "Send additional instructions such as /pause, /resume, /status, or scope updates to a running Cursor tmux session.",
    parameters: SEND_SCHEMA,
    async execute(_toolCallId, params) {
      try {
        return await handleSendTool(config, params || {});
      } catch (error) {
        return textResult(`发送指令失败: ${error instanceof Error ? error.message : String(error)}`, {}, true);
      }
    },
  });

  api.registerTool({
    name: "cursor_agent_kill_session",
    label: "Cursor Agent Kill Session",
    description: "Gracefully stop or force kill a Cursor tmux session managed by the cursor-agent-system toolkit.",
    parameters: KILL_SCHEMA,
    async execute(_toolCallId, params) {
      try {
        return await handleKillTool(config, params || {});
      } catch (error) {
        return textResult(`结束会话失败: ${error instanceof Error ? error.message : String(error)}`, {}, true);
      }
    },
  });

  api.registerTool({
    name: "cursor_agent_doctor",
    label: "Cursor Agent Doctor",
    description: "Inspect plugin configuration, toolkit paths, and shell execution mode for the cursor-agent-system plugin.",
    parameters: { type: "object", properties: {} },
    async execute() {
      try {
        const report = await diagnose(config);
        return jsonResult(report, !report.ok);
      } catch (error) {
        return textResult(`诊断失败: ${error instanceof Error ? error.message : String(error)}`, {}, true);
      }
    },
  });
}

function registerCommands(api, config) {
  api.registerCommand({
    name: "cursor",
    description: "管理 OpenClaw Cursor Agent 后台任务（doctor/list/status/send/kill/spawn）",
    acceptsArgs: true,
    requireAuth: false,
    async handler(ctx) {
      const raw = asString(ctx.args || "").trim();
      if (!raw || raw === "help") {
        return {
          text: [
            "用法:",
            "/cursor doctor",
            "/cursor list",
            "/cursor status <会话名或任务别名>",
            "/cursor send <会话名或任务别名> <指令>",
            "/cursor kill <会话名或任务别名> [--force]",
            "/cursor spawn <任务名> || <任务描述> || [项目路径]",
          ].join("\n"),
        };
      }

      const [subcommand, ...rest] = raw.split(/\s+/);
      const tail = raw.slice(subcommand.length).trim();

      try {
        if (subcommand === "doctor") {
          const report = await diagnose(config);
          return { text: JSON.stringify(report, null, 2) };
        }
        if (subcommand === "list") {
          const result = await handleListTool(config);
          return { text: result.content[0].text };
        }
        if (subcommand === "status") {
          const result = await handleStatusTool(config, tail);
          return { text: result.content[0].text };
        }
        if (subcommand === "send") {
          const [sessionQuery, ...commandParts] = rest;
          const result = await handleSendTool(config, {
            sessionQuery,
            command: commandParts.join(" "),
          });
          return { text: result.content[0].text };
        }
        if (subcommand === "kill") {
          const force = rest.includes("--force");
          const sessionQuery = rest.filter((item) => item !== "--force")[0];
          const result = await handleKillTool(config, {
            sessionQuery,
            force,
            purge: false,
          });
          return { text: result.content[0].text };
        }
        if (subcommand === "spawn") {
          const parts = tail.split("||").map((item) => item.trim()).filter(Boolean);
          if (parts.length < 2) {
            return { text: "spawn 用法: /cursor spawn <任务名> || <任务描述> || [项目路径]" };
          }
          const [taskName, taskDescription, projectPath] = parts;
          const result = await handleSpawnTool(config, { taskName, taskDescription, projectPath });
          return { text: result.content[0].text };
        }
        return { text: "未知子命令。请使用 /cursor help 查看帮助。" };
      } catch (error) {
        return { text: `执行失败: ${error instanceof Error ? error.message : String(error)}` };
      }
    },
  });
}

function registerCli(api, config) {
  if (!api.registerCli) {
    return;
  }
  api.registerCli((ctx) => {
    ctx.program
      .command("cursor-agent-doctor")
      .description("检查 OpenClaw Cursor Agent 插件配置和工具目录")
      .action(async () => {
        const report = await diagnose(config);
        console.log(JSON.stringify(report, null, 2));
        if (!report.ok) {
          process.exitCode = 1;
        }
      });

    ctx.program
      .command("cursor-agent-list")
      .description("列出 Cursor 后台任务会话")
      .action(async () => {
        const result = await handleListTool(config);
        console.log(result.content[0].text);
      });
  }, { commands: ["cursor-agent-doctor", "cursor-agent-list"] });
}

const plugin = {
  id: PLUGIN_ID,
  name: "OpenClaw Cursor Agent",
  description: "Manage persistent Cursor CLI coding tasks through tmux.",
  register(api) {
    const config = normalizeConfig(api.pluginConfig);
    registerTools(api, config);
    if (api.registerCommand) {
      registerCommands(api, config);
    }
    registerCli(api, config);
  },
};

export default plugin;


path: extensions/openclaw-cursor-agent/openclaw.plugin.json
content: {
  "id": "openclaw-cursor-agent",
  "name": "OpenClaw Cursor Agent",
  "description": "Manage persistent Cursor CLI coding tasks through tmux from OpenClaw tools and commands.",
  "version": "0.1.0",
  "skills": ["./skill"],
  "configSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "toolkitRoot": {
        "type": "string",
        "description": "Absolute path of the cursor-agent-system directory."
      },
      "defaultProjectPath": {
        "type": "string",
        "description": "Default project directory used when spawn requests omit projectPath."
      },
      "executionMode": {
        "type": "string",
        "enum": ["direct", "wsl"],
        "default": "direct",
        "description": "direct: call bash directly; wsl: call scripts through wsl.exe bash."
      },
      "timeoutMs": {
        "type": "number",
        "default": 120000,
        "minimum": 1000
      },
      "shell": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "executable": {
            "type": "string",
            "default": "bash"
          },
          "args": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "default": []
          },
          "workingDirectory": {
            "type": "string",
            "default": ""
          },
          "wslDistro": {
            "type": "string",
            "default": ""
          }
        }
      }
    }
  }
}


path: extensions/openclaw-cursor-agent/package.json
content: {
  "name": "openclaw-cursor-agent",
  "version": "0.1.0",
  "type": "module",
  "description": "OpenClaw plugin for managing tmux-backed Cursor CLI tasks.",
  "main": "index.js",
  "openclaw": {
    "extensions": [
      "./index.js"
    ]
  },
  "devDependencies": {
    "openclaw": "^2026.3.2"
  }
}


path: extensions/openclaw-cursor-agent/README.md
content: # openclaw-cursor-agent

OpenClaw plugin for managing persistent Cursor CLI coding tasks through tmux sessions.

## Architecture

```text
OpenClaw → plugin → WSL → tmux → Cursor CLI Agent
```

## Tools

| Tool | Description |
|------|-------------|
| `cursor_agent_spawn_task` | Start a background Cursor coding task |
| `cursor_agent_list_tasks` | List all recorded tasks |
| `cursor_agent_check_status` | Check task status, progress, and output |
| `cursor_agent_send_command` | Send follow-up instructions to a running task |
| `cursor_agent_kill_session` | Stop a running task |
| `cursor_agent_doctor` | Diagnose environment and dependencies |

## Chat Commands

```
/cursor doctor              — Check environment
/cursor list                — List tasks
/cursor status <session>    — Check task status
/cursor send <session> <msg> — Send instruction
/cursor kill <session>       — Stop task
/cursor spawn <name> || <description> || [path]
```

## Configuration

In `openclaw.json`:

```json
{
  "openclaw-cursor-agent": {
    "enabled": true,
    "config": {
      "toolkitRoot": "C:/Users/YOU/.openclaw/workspace/cursor-agent-system",
      "defaultProjectPath": "D:/project/your-project",
      "executionMode": "wsl",
      "timeoutMs": 120000,
      "shell": {
        "executable": "C:/Windows/System32/wsl.exe",
        "args": [],
        "wslDistro": "Debian"
      }
    }
  }
}
```

## WSL Dependencies

Inside WSL, ensure these are installed:

- `bash`, `tmux`, `python3` (via `apt`)
- Cursor CLI (`agent`) with valid login
- Node.js (if Cursor CLI bundled binary is incompatible)


path: extensions/openclaw-cursor-agent/skill/references/commands.md
content: # `/cursor` Command Patterns

Use these chat commands when the plugin is installed:

```text
/cursor doctor
/cursor list
/cursor status myproj-auth-20260318-180000
/cursor send myproj-auth-20260318-180000 /status
/cursor send myproj-auth-20260318-180000 把 JWT 改成 RS256
/cursor kill myproj-auth-20260318-180000 --force
/cursor spawn feature-auth || 实现 JWT 登录接口 || /mnt/d/project/myapp
```

## Recommendations

- Prefer tools over chat commands when the caller is another agent or workflow.
- Prefer `/cursor doctor` before the first production use.
- In Windows + WSL mode, `projectPath` should use the Linux path form such as `/mnt/d/project/...`.


path: extensions/openclaw-cursor-agent/examples/openclaw.json.windows.example.json
content: {
  "plugins": {
    "allow": ["openclaw-cursor-agent"],
    "load": {
      "paths": [
        "C:/Users/YOUR_USER/.openclaw/workspace/plugins/openclaw-cursor-agent"
      ]
    },
    "entries": {
      "openclaw-cursor-agent": {
        "enabled": true,
        "config": {
          "toolkitRoot": "C:/Users/YOUR_USER/.openclaw/workspace/cursor-agent-system",
          "defaultProjectPath": "D:/project/your-project",
          "executionMode": "wsl",
          "timeoutMs": 120000,
          "shell": {
            "executable": "C:/Windows/System32/wsl.exe",
            "args": [],
            "wslDistro": "Debian"
          }
        }
      }
    }
  }
}


path: docs/LOCAL_SETUP_GUIDE.md
content: # 小虾本地部署完整配置指南

> 从腾讯云迁移到本地的完整步骤

---

## 📋 目录

1. [环境准备](#1-环境准备)
2. [安装 OpenClaw](#2-安装-openclaw)
3. [恢复备份数据](#3-恢复备份数据)
4. [网络配置](#4-网络配置)
5. [飞书 Bot 配置](#5-飞书-bot-配置)
6. [启动与验证](#6-启动与验证)
7. [常见问题](#7-常见问题)

---

## 1. 环境准备

### 1.1 硬件要求

| 项目 | 最低要求 | 推荐配置 |
|------|---------|---------|
| CPU | 2核 | 4核+ |
| 内存 | 4GB | 8GB+ |
| 存储 | 20GB 可用空间 | 50GB+ SSD |
| 网络 | 能访问互联网 | 公网 IP 或 Tailscale |
| 运行时间 | - | 24小时开机 |

**推荐设备**: Mac Mini / 树莓派4B / NAS / 旧笔记本

### 1.2 系统要求

- **macOS**: 12+ (Monterey 或更新)
- **Linux**: Ubuntu 20.04+ / Debian 11+ / CentOS 8+
- **Windows**: Windows 10/11 + WSL2

### 1.3 安装依赖

#### macOS

```bash
# 安装 Homebrew（如未安装）
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# 安装 Node.js 22
brew install node@22

# 安装 ffmpeg（用于语音/视频功能）
brew install ffmpeg

# 验证安装
node --version  # v22.x.x
npm --version   # 10.x.x
```

#### Ubuntu/Debian

```bash
# 更新系统
sudo apt update && sudo apt upgrade -y

# 安装 Node.js 22
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs

# 安装 ffmpeg
sudo apt install -y ffmpeg

# 验证安装
node --version
npm --version
```

#### Windows (WSL2)

```bash
# 在 WSL2 Ubuntu 中执行
sudo apt update
sudo apt install -y curl

# 安装 Node.js 22
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs ffmpeg

# 验证
node --version
```

---

## 2. 安装 OpenClaw

```bash
# 全局安装 OpenClaw
npm install -g openclaw

# 验证安装
openclaw --version

# 查看帮助
openclaw --help
```

---

## 3. 恢复备份数据

### 3.1 下载备份包

```bash
# 创建下载目录
mkdir -p ~/Downloads/xiaoxia-backup
cd ~/Downloads/xiaoxia-backup

# 下载备份包（514MB，可能需要几分钟）
curl -L -o xiaoxia-full-backup.tar.gz \
  https://github.com/zhangzeyu99-web/xiaoxia-memory/releases/download/backup-20260318-full/xiaoxia-full-backup-20260318_112254.tar.gz

# 下载校验文件
curl -L -o xiaoxia-full-backup.tar.gz.sha256 \
  https://github.com/zhangzeyu99-web/xiaoxia-memory/releases/download/backup-20260318-full/xiaoxia-full-backup-20260318_112254.tar.gz.sha256

# 验证文件完整性
sha256sum -c xiaoxia-full-backup.tar.gz.sha256
# 应显示: xiaoxia-full-backup.tar.gz: OK
```

### 3.2 解压备份

```bash
# 解压
tar xzvf xiaoxia-full-backup.tar.gz

# 进入解压后的目录
cd xiaoxia-full-backup-20260318_112254

# 查看内容
ls -la
```

### 3.3 执行恢复脚本

```bash
# 确保 ~/.openclaw 目录存在
mkdir -p ~/.openclaw

# 运行恢复脚本
./restore.sh
```

**恢复脚本会自动复制**:
- `openclaw.json` → `~/.openclaw/`
- `workspace/` → `~/.openclaw/`
- `agents/` → `~/.openclaw/`
- `extensions/` → `~/.openclaw/`
- `autoskill/` → `~/.openclaw/`
- `sessions/` → `~/.openclaw/`
- `cron.json` → `~/.openclaw/`
- `nodes/` → `~/.openclaw/`

### 3.4 验证恢复

```bash
# 检查目录结构
ls -la ~/.openclaw/

# 应显示:
# agents/
# autoskill/
# cron.json
# extensions/
# nodes/
# openclaw.json
# sessions/
# workspace/
```

---

## 4. 网络配置

### 方案 A: Tailscale（推荐）

**优点**: 无需公网IP，自动组网，安全可靠

#### 4.1.1 安装 Tailscale

**macOS**:
```bash
# 从 App Store 安装 Tailscale
# 或命令行
brew install tailscale
sudo tailscale up
```

**Linux**:
```bash
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
```

#### 4.1.2 登录 Tailscale

```bash
# 启动 Tailscale
sudo tailscale up

# 按提示在浏览器中登录账号
# 确保和云端服务器登录同一个账号
```

#### 4.1.3 获取 Tailscale IP

```bash
# 查看本机 Tailscale IP
tailscale ip -4
# 示例输出: 100.x.x.x
```

#### 4.1.4 修改 OpenClaw 配置

```bash
# 编辑配置文件
nano ~/.openclaw/openclaw.json
```

**修改以下字段**:

```json
{
  "gateway": {
    "port": 18789,
    "mode": "local",
    "bind": "0.0.0.0",
    "controlUi": {
      "allowedOrigins": [
        "http://100.x.x.x:18789"
      ],
      "allowInsecureAuth": false,
      "dangerouslyDisableDeviceAuth": false
    }
  }
}
```

将 `100.x.x.x` 替换为你实际的 Tailscale IP。

---

### 方案 B: 公网 IP + 端口转发

如果你有公网 IP:

#### 4.2.1 配置路由器端口转发

```
路由器后台 → 端口转发/虚拟服务器
外部端口: 18789
内部 IP: 你的本地机器IP
内部端口: 18789
协议: TCP
```

#### 4.2.2 修改 OpenClaw 配置

```bash
nano ~/.openclaw/openclaw.json
```

```json
{
  "gateway": {
    "port": 18789,
    "mode": "local", 
    "bind": "0.0.0.0",
    "controlUi": {
      "allowedOrigins": [
        "http://你的公网IP:18789"
      ]
    }
  }
}
```

#### 4.2.3 DDNS（动态域名）

如果公网 IP 会变化，建议使用 DDNS:
- 花生壳
- No-IP
- 阿里云 DDNS

---

### 方案 C: FRP 内网穿透

如果你没有公网 IP，可用 FRP:

**frps.ini（云服务器）**:
```ini
[common]
bind_port = 7000
[REDACTED_SECRET]
```

**frpc.ini（本地机器）**:
```ini
[common]
server_addr = 你的云服务器IP
server_port = 7000
[REDACTED_SECRET]

[openclaw]
type = tcp
local_port = 18789
remote_port = 18789
```

---

## 5. 飞书 Bot 配置

### 5.1 登录飞书开发者平台

访问: https://open.feishu.cn/app/

### 5.2 修改事件订阅地址

对每个 Bot 应用执行:

#### bot-xiaoxia（小虾）

1. 找到应用: **bot-xiaoxia**
2. 进入: **事件与回调** → **事件订阅**
3. 修改 **请求地址 URL**:

```
旧: http://43.162.108.47:18789/webhook/feishu
新: http://你的IP:18789/webhook/feishu

# 使用 Tailscale 时:
http://100.x.x.x:18789/webhook/feishu
```

4. 点击 **保存**
5. 验证连接（飞书会发送验证请求）

#### bot-xiaopin（狃姐）

同上，修改 webhook URL

#### bot-xiaoyi（小伊）

同上，修改 webhook URL

### 5.3 验证 Webhook 可访问

```bash
# 从其他机器测试（或用手机流量）
curl http://你的IP:18789/status

# 应返回类似:
{"status":"ok","version":"2026.3.x"}
```

---

## 6. 启动与验证

### 6.1 启动 Gateway

```bash
# 启动服务
openclaw gateway start

# 或使用后台模式
nohup openclaw gateway start > ~/openclaw-gateway.log 2>&1 &
```

### 6.2 检查状态

```bash
# 查看 OpenClaw 状态
openclaw status

# 查看 Gateway 状态
openclaw gateway status

# 查看定时任务
openclaw cron list
```

### 6.3 测试消息收发

1. **飞书私聊测试**:
   - 给小虾发送: "你好"
   - 应收到回复

2. **子 Agent 测试**:
   - @狃姐 测试
   - @小伊 测试

3. **定时任务测试**:
   ```bash
   # 手动触发早报测试
   openclaw cron run 晚星早报
   ```

### 6.4 验证 Session 历史

```bash
# 查看会话列表
openclaw sessions list

# 查看特定会话历史
openclaw sessions history <session-key>
```

---

## 7. 常见问题

### Q1: 启动时报 "port already in use"

```bash
# 查找占用进程
lsof -i :18789

# 或
netstat -tlnp | grep 18789

# 结束占用进程
kill -9 <PID>

# 或修改端口
# 编辑 ~/.openclaw/openclaw.json
# 修改 gateway.port 为其他端口（如 18790）
```

### Q2: 飞书收不到消息

**排查步骤**:

1. **检查 Gateway 是否运行**:
   ```bash
   openclaw gateway status
   ```

2. **检查端口是否可访问**:
   ```bash
   # 本地测试
curl http://localhost:18789/status
   
   # 外部测试（从手机或其他网络）
   curl http://你的IP:18789/status
   ```

3. **检查防火墙**:
   ```bash
   # macOS
   sudo /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate
   
   # Linux
   sudo ufw status
   sudo iptables -L | grep 18789
   ```

4. **查看飞书开发者平台日志**:
   - 事件与回调 → 查看事件日志

### Q3: 插件加载失败

```bash
# 重新安装插件依赖
cd ~/.openclaw/extensions/openclaw-lark
npm install

cd ~/.openclaw/extensions/a2a-gateway
npm install
```

### Q4: Session 历史丢失

```bash
# 检查 sessions 目录
ls -la ~/.openclaw/sessions/

# 如果为空，从备份重新复制
cp -r ~/Downloads/xiaoxia-backup/xiaoxia-full-backup-*/sessions ~/.openclaw/
```

### Q5: 定时任务不执行

```bash
# 检查 cron 配置
openclaw cron list

# 重新添加任务
openclaw cron add --cron "0 9 * * *" --agent main --session isolated \
  --message "执行早报任务" --name "晚星早报"
```

### Q6: 启动时提示权限不足

```bash
# 修复权限
sudo chown -R $(whoami) ~/.openclaw

# 或使用 sudo 启动（不推荐）
sudo openclaw gateway start
```

### Q7: Tailscale 连接不上

```bash
# 检查 Tailscale 状态
tailscale status

# 重新登录
tailscale up --force-reauth

# 检查防火墙是否放行
tailscale netcheck
```

---

## 📞 需要帮助？

如果遇到其他问题:

1. **查看日志**:
   ```bash
   openclaw logs
   # 或
   cat ~/openclaw-gateway.log
   ```

2. **检查配置**:
   ```bash
   openclaw config validate
   ```

3. **重置 Gateway**:
   ```bash
   openclaw gateway stop
   openclaw gateway start
   ```

4. **联系小虾**:
   - 飞书私聊 @小虾
   - 或发送消息到关联的飞书 Bot

---

## ✅ 完成清单

迁移完成后检查:

- [ ] OpenClaw 安装完成
- [ ] 备份数据已恢复
- [ ] 网络配置完成（Tailscale/公网IP/FRP）
- [ ] 飞书 Webhook 已更新
- [ ] Gateway 正常启动
- [ ] 飞书消息能正常收发
- [ ] 子 Agent（狃姐、小伊）响应正常
- [ ] Session 历史可访问
- [ ] 定时任务列表显示正常
- [ ] 测试消息收发成功

---

**祝你迁移顺利！** 🦞

如有问题随时联系小虾。


path: docs/usage-guide.md
content: # OpenClaw Cursor Agent 使用指南

## 一句话理解

你把 OpenClaw 当成项目经理，对它说需求，它会在后台调度 Cursor 去干活。

## 当前架构

```text
你（飞书/聊天） → OpenClaw Agent → 插件 → WSL Debian → tmux → Cursor CLI Agent
```

你不需要关心 tmux、WSL、agent 这些底层概念，直接用自然语言说需求即可。

## 你可以这么说

### 启动后台任务

- 帮我用 Cursor 在后台做一个任务：给项目补 README，不要动其他文件。
- 用 Cursor 后台在 `D:/project/openclaw` 里实现 JWT 登录接口。

### 查看任务进度

- 看看刚才那个 Cursor 任务做到哪了。
- 把刚才那个任务的最新输出给我看一下。

### 给任务补充要求

- 告诉刚才那个 Cursor 任务：JWT 改成 RS256。
- 让它先别写测试，先把接口实现完。

### 暂停 / 恢复 / 结束

- 先暂停刚才那个 Cursor 任务。
- 恢复刚才那个 Cursor 任务。
- 把刚才那个 Cursor 任务停掉。

## 完整使用流程

1. 说需求 → OpenClaw 启动 Cursor 后台任务
2. 随时追问进度
3. 中途补充要求
4. 做完后结束任务

## 给 OpenClaw Agent 的行为规范

当用户说需求时，调用 `cursor_agent_spawn_task`。
当用户问进度时，调用 `cursor_agent_check_status`。
当用户补充要求时，调用 `cursor_agent_send_command`。
当用户要停止时，调用 `cursor_agent_kill_session`。
不确定环境是否正常时，先调用 `cursor_agent_doctor`。

不要默认回退到"先装 XXX"——除非 doctor 明确报错。

## 诊断优先级

当不确定环境是否可用时，按这个顺序判断：

1. 插件是否已加载 → `cursor_agent_doctor`
2. shell runner 是否可用
3. `tmux` / `python3` / `agent` 是否可用
4. 运行目录 `status/tasks/logs` 是否存在

不要跳过检查直接下结论。


path: docs/archive/WSL最终落地方案.md
content: # WSL 最终落地方案

这份文档用于把当前的 Windows + Git Bash 兼容模式，升级到更稳定的 WSL 方案。

## 当前状态

已完成：

- `Microsoft-Windows-Subsystem-Linux` 已启用
- `VirtualMachinePlatform` 已启用

当前机器状态：

- 两项功能都已写入系统
- `RestartNeeded = True`

也就是说，**下一步必须先重启系统**。

## 总体目标

把当前执行链路切成：

```text
OpenClaw -> openclaw-cursor-agent -> WSL -> tmux -> Cursor CLI Agent
```

这样可以绕开当前 Windows 原生 Cursor shell tool 的兼容问题。

## Phase 1：系统重启

先重启 Windows。

重启后先验证：

```powershell
wsl --status
wsl --list --verbose
```

如果 `wsl` 命令已可用，继续下一步。

## Phase 2：安装一个发行版

推荐安装 Ubuntu 或 Debian。

如果系统支持：

```powershell
wsl --install -d Ubuntu
```

如果该命令不可用，则改走手动安装发行版方式。

目标是让下面命令可用：

```powershell
wsl -d Ubuntu -- uname -a
```

## Phase 3：在 WSL 内安装 tmux

进入 WSL 后执行：

```bash
sudo apt update
sudo apt install -y tmux
tmux -V
```

目标：

- `tmux` 在 WSL 内原生可用

## Phase 4：在 WSL 内安装 Cursor CLI

在 WSL 中执行：

```bash
curl https://cursor.com/install -fsS | bash
agent --version
```

如果需要登录：

```bash
agent login
```

目标：

- `agent` 在 WSL 内原生可用

## Phase 5：验证 WSL 内三件套

确认这三项都通过：

```bash
command -v bash
command -v tmux
command -v agent
```

## Phase 6：切回插件 WSL 模式

将 `openclaw.json` 中的 `openclaw-cursor-agent` 配置改成：

```json
{
  "enabled": true,
  "config": {
    "toolkitRoot": "C:/Users/Administrator/.openclaw/workspace/cursor-agent-system",
    "defaultProjectPath": "D:/project/openclaw",
    "executionMode": "wsl",
    "timeoutMs": 120000,
    "shell": {
      "executable": "C:/Windows/System32/wsl.exe",
      "args": [],
      "workingDirectory": "",
      "wslDistro": "Ubuntu"
    }
  }
}
```

如果发行版不是 Ubuntu，把 `wslDistro` 改成实际名称。

## Phase 7：验证插件 doctor

切换后运行：

```powershell
openclaw cursor-agent-doctor
```

理想结果：

- `executionMode = wsl`
- `shell.executable = C:/Windows/System32/wsl.exe`
- `python3 ok`
- `tmux ok`
- `agent ok`
- `issues = []`

## Phase 8：执行最小 smoke test

先跑一个极简任务：

```text
帮我用 Cursor 在后台做一个任务：只回复一句 hello from wsl cursor agent，不要修改任何文件。
```

确认：

- 能启动任务
- 能输出开始执行
- 能正常完成

## Phase 9：执行 Git 状态检查任务

然后再跑：

```text
帮我用 Cursor 在后台做一个任务：检查 D:/project/windows-gateway-background 的 git status、git remote -v 和 README 是否存在，只检查，不要修改文件。
```

确认：

- `git status` 真正返回结果
- `git remote -v` 真正返回结果
- README 检查完成

## Phase 10：执行 GitHub 推送任务

最后再跑完整目标任务：

```text
帮我用 Cursor 在后台做一个任务：检查 D:/project/windows-gateway-background 是否已经推送到 GitHub；如果没有，就创建新仓库、补 README、推送代码，最后返回仓库链接。
```

## 推荐验收顺序

按这个顺序验收最稳：

1. `wsl --status`
2. WSL 中 `tmux -V`
3. WSL 中 `agent --version`
4. `openclaw cursor-agent-doctor`
5. 最小 smoke test
6. Git 状态检查任务
7. GitHub 推送任务

## 为什么 WSL 方案更稳

因为当前 Git Bash 兼容模式虽然已经打通：

- OpenClaw 调度
- tmux 后台会话
- Cursor Agent 启动
- 流式输出

但最后仍卡在：

- Cursor CLI Windows 原生 shell tool 执行行为

WSL 的意义就是把最后这一层 shell / git 执行环境切到真正的 Linux 用户态，从根上绕开 Windows 原生兼容问题。

## 实际执行记录（2026-03-19 更新）

所有 Phase 已完成：

| Phase | 状态 | 备注 |
|-------|------|------|
| Phase 1: 系统重启 | ✅ 完成 | WSL 功能已启用 |
| Phase 2: 安装发行版 | ✅ 完成 | Debian (WSL1)，通过 Appx 手动安装 |
| Phase 3: 安装 tmux | ✅ 完成 | tmux 3.1c via apt |
| Phase 4: 安装 Cursor CLI | ✅ 完成 | 通过自定义包装脚本 + Node.js 22 |
| Phase 5: 三件套验证 | ✅ 完成 | bash + tmux + agent 全部可用 |
| Phase 6: 插件切 WSL 模式 | ✅ 完成 | `executionMode: "wsl"`, `wslDistro: "Debian"` |
| Phase 7: 插件 doctor | ✅ 完成 | check-status.sh 正确返回所有任务 |
| Phase 8: 最小 smoke test | ✅ 完成 | agent 成功创建 hello.txt |
| Phase 9: spawn 端到端测试 | ✅ 完成 | `wsl-smoke-final` 任务完成，exit_code=0 |

### 遇到的额外问题及修复

1. **Windows 10 17763 仅支持 WSL1**：`wsl --install` 不可用，改为手动下载 Debian Appx 包安装。
2. **Cursor CLI 自带 Node 二进制不兼容 WSL1**：改为手动安装 Node.js 22 Linux x64 + 自定义包装脚本。
3. **包装脚本缺少 `"$@"`**：子命令（login/status）被吞掉，已修复。
4. **CRLF 换行符**：Windows 文件复制到 WSL 后 shebang 失效，用 `tr -d '\r'` 修复。
5. **PYTHONIOENCODING 容错**：从 `utf-8` 改为 `utf-8:replace`，避免非 UTF-8 字节导致 Python 崩溃。
6. **WSL Debian 缺少 python3**：通过 `apt-get install python3` 补齐。

## 一句话结论

**WSL 方案已全部落地并验证通过。** OpenClaw → WSL Debian → tmux → Cursor CLI Agent 端到端链路稳定运行。


path: docs/archive/最终测试报告.md
content: # OpenClaw Cursor Agent 最终测试报告

日期：2026-03-18

## 目标

验证以下链路是否可用：

```text
OpenClaw -> openclaw-cursor-agent -> shell -> tmux -> Cursor CLI Agent
```

并确认是否能在当前 Windows 机器上完成后台 Cursor 任务执行。

## 测试环境

- 系统：Windows 10.0.17763
- Git Bash：`D:/Git/bin/bash.exe`
- tmux：已手工补入 Git for Windows 运行环境
- Cursor CLI：Windows 原生安装，Git Bash 中通过包装入口调用
- OpenClaw 插件：`openclaw-cursor-agent`

## 已打通部分

以下链路已经验证通过：

1. OpenClaw 能启动后台任务
2. 插件能创建 tmux 会话
3. Git Bash 可以作为 shell 执行层
4. Cursor CLI Agent 能在后台启动
5. 状态脚本能读取 tmux 输出
6. 流式 `stream-json` 输出可见
7. 自然语言任务可以被正确转换为后台任务

## 关键修复项

### 1. 启动命令修复

旧方案把整份 Markdown 任务文件直接拼进一条命令行，容易被 Bash 解析炸掉。

已修复为：

- 先生成独立 `runner.sh`
- 再由 tmux 执行 `runner.sh`

### 2. 编码问题修复

为 Git Bash / Python 输出补上：

- `PYTHONIOENCODING=utf-8`
- `PYTHONUTF8=1`
- `LANG=C.UTF-8`
- `LC_ALL=C.UTF-8`

### 3. Windows 路径兼容修复

项目路径会在需要时转换成 MSYS 路径，避免 tmux / bash 落到错误目录。

### 4. Headless 输出修复

已切换到：

- `--print`
- `--output-format stream-json`
- `--stream-partial-output`

这样后台任务不再是“完全无输出黑盒”。

### 5. Workspace 参数修复

已显式传入 `--workspace`，避免 Cursor Agent 误判项目目录不在当前工作区。

### 6. 状态检测增强

`doctor` 和 `check-status.sh` 已增强：

- 可区分 shell 存活和 agent 真实执行
- 能显示依赖层状态
- 能显示脚本目录、运行目录、shell runner、`python3`、`tmux`、`agent`

## 成功样例

以下 smoke test 已成功完成：

- `openclaw-smoke-test-20260318-194422`
- `openclaw-quick-parse-test-20260318-203713`

表现：

- 能正常输出“开始执行”
- 能返回任务结果
- 能输出 `agent_exit_code=0`

## 未完全打通部分

当前仍存在一个关键限制：

### Cursor CLI Windows 原生 shell tool 执行异常

在 Git Bash 兼容模式下，Cursor Agent 本身可以启动并流式输出，但其内部发起的 shell 工具调用仍存在异常。

已观察到的问题：

- `pwd`
- `echo hello-shell-test`
- `git status`
- `git remote -v`

在 Cursor 的 shell tool 中会出现：

- 调用开始
- 长时间无标准输出
- 30s / 60s 超时
- `stdout` 为空

而这些命令在宿主机直接执行时是正常的。

## 当前结论

### 结论 1

`OpenClaw -> 插件 -> Git Bash -> tmux -> Cursor CLI Agent` 这条链路本身已经成立。

### 结论 2

当前剩余阻塞点不在插件脚本层，而在：

`Cursor CLI Windows 原生 shell tool 行为`

### 结论 3

如果目标只是验证：

- 后台任务能否被拉起
- 是否能持续监控
- 是否能追加指令

那么流程已经跑通。

### 结论 4

如果目标是让 Cursor 后台任务在这台 Windows 机器上稳定执行 Git / shell 类操作，则仍建议切换到 WSL。

## 测试任务保留情况

以下任务材料已保留，可用于 OpenClaw 回归测试：

- `windows-gateway-background-check-push-github-20260318-202546`
- `windows-gateway-background-check-git-status-only-20260318-205526`
- `windows-gateway-background-check-git-status-v4-20260318-210651`

说明：

- 旧失败样本保留任务文件和日志
- 新 v4 样本保留用于继续观察/回归

## WSL 最终落地结果（2026-03-19 更新）

### WSL 环境

- 发行版：Debian (WSL1)
- tmux：3.1c（apt 安装）
- Node.js：v22.22.1（官方 Linux x64 二进制）
- Cursor CLI：2026.03.11-6dfa30c（通过自定义包装脚本调用）
- Python：3.9.2（apt 安装）
- 认证状态：已通过浏览器完成 `agent login`

### 关键修复

1. **包装脚本缺少 `"$@"`**：原始 `/usr/local/bin/agent` 脚本末尾缺少参数透传，导致 `login`、`status` 等子命令被吞掉。已修复。
2. **Windows CRLF 换行符**：从 Windows 复制到 WSL 的脚本带有 `\r`，导致 shebang 找不到 `bash\r`，退回到 `sh` 执行。已用 `tr -d '\r'` 修复。
3. **Python 编码容错**：tmux 输出包含非 UTF-8 字节，`PYTHONIOENCODING` 从 `utf-8` 改为 `utf-8:replace`。
4. **WSL 缺少 python3**：Debian 最小安装未包含 Python，已通过 `apt-get install python3` 补齐。

### WSL 冒烟测试结果

| 测试项 | 结果 |
|--------|------|
| `agent status`（认证检查） | ✅ 已认证 |
| `agent` 直接执行简单任务 | ✅ 成功创建 hello.txt |
| `check-status.sh --json`（列出任务） | ✅ 正确返回所有历史任务 |
| `spawn-cursor.sh` 启动后台任务 | ✅ 成功启动 tmux 会话 |
| `check-status.sh <session>` 查询状态 | ✅ 返回 completed / 100% |
| 文件实际写入验证 | ✅ `wsl-test-result.txt` 内容正确 |

### WSL 端到端验证任务

- 任务 ID：`openclaw-wsl-smoke-final-20260318-235045`
- Agent 退出码：`0`
- Agent 耗时：约 30 秒
- 产出文件：`D:\project\openclaw\wsl-test-result.txt`
- 链路验证：`OpenClaw → tmux 会话 → Cursor CLI Agent → 文件写入 → 完成`

### 最终结论

1. **WSL 方案已完全打通。** Git Bash 兼容模式下的 Cursor CLI shell tool 超时问题在 WSL 中不复存在。
2. 插件配置已切换为 `executionMode: "wsl"`，`wslDistro: "Debian"`。
3. 从 OpenClaw 到 tmux 到 Cursor CLI Agent 的全链路在 WSL 中稳定运行。
4. 后续所有后台 Cursor 任务建议统一走 WSL 通道。


path: cursor-agent-system/README.md
content: # OpenClaw Cursor Agent System

这是一个把 `OpenClaw -> tmux -> Cursor CLI` 串起来的三层执行架构，目标是让长时编码任务可以在后台持续运行、可监控、可干预、可回连。

## 目录结构

```text
cursor-agent-system/
├── README.md
├── scripts/
│   ├── common.sh
│   ├── spawn-cursor.sh
│   ├── check-status.sh
│   ├── attach-session.sh
│   ├── send-command.sh
│   └── kill-session.sh
├── templates/
│   └── cursor-task-prompt.md
├── status/
├── tasks/
└── logs/
```

## 环境要求

- `tmux` 3.3+
- `agent` 或 `cursor-agent`
- `python3`
- `bash`

Windows 原生不能直接跑 `tmux`，建议在 WSL 里使用本套脚本。

## 脚本说明

### 1. 启动任务

```bash
./scripts/spawn-cursor.sh \
  feature-auth \
  "实现 JWT 登录接口并补充测试" \
  /path/to/project \
  --priority high \
  --eta 45分钟
```

功能：

- 自动生成唯一会话名：`{project}-{task}-{timestamp}`
- 自动创建任务文件、状态文件、日志文件
- 在指定项目目录下创建 tmux 会话
- 通过真实 TTY 启动 Cursor CLI

### 2. 查看状态

```bash
./scripts/check-status.sh
./scripts/check-status.sh feature-auth
./scripts/check-status.sh myproj-feature-auth-20260318-180000
```

功能：

- 无参数时列出所有已记录任务
- 有参数时输出状态文件、tmux PID 和最近 30 行输出
- 自动回写状态快照

### 3. 实时附加

```bash
./scripts/attach-session.sh myproj-feature-auth-20260318-180000
```

退出方式：`Ctrl+B, D`

### 4. 发送额外指令

```bash
./scripts/send-command.sh myproj-feature-auth-20260318-180000 "/status"
./scripts/send-command.sh myproj-feature-auth-20260318-180000 "/focus 登录 API"
./scripts/send-command.sh myproj-feature-auth-20260318-180000 "把 JWT 签名算法改成 RS256"
```

### 5. 结束会话

```bash
./scripts/kill-session.sh myproj-feature-auth-20260318-180000
./scripts/kill-session.sh myproj-feature-auth-20260318-180000 --force
./scripts/kill-session.sh myproj-feature-auth-20260318-180000 --force --purge --yes
```

默认行为是先发送 `Ctrl+C` 尝试优雅退出；如果仍未结束，再使用 `--force` 强制终止。

## 状态文件约定

每个任务都会在 `status/` 下生成一个同名 JSON 文件，文件名与 tmux 会话名一致。这样 `check-status.sh`、`send-command.sh`、`kill-session.sh` 都可以直接通过会话名或任务别名解析到同一个状态文件。

核心字段包括：

- `taskId`
- `taskName`
- `sessionName`
- `status`
- `projectPath`
- `priority`
- `estimatedDuration`
- `progress.currentStep`
- `progress.completedSteps`
- `output.lastCapture`

## 与原型脚本相比的改进

- 用统一的 `common.sh` 收敛公共逻辑
- 避免依赖 `find + grep + head` 去猜测状态文件
- 支持用任务别名自动解析最近一次会话
- `spawn-cursor.sh` 支持 `--priority`、`--eta`、`--task-file`
- `check-status.sh` 支持批量列出全部任务
- `kill-session.sh` 提供确认、强制终止和清理文件选项

## 推荐工作流

1. 在 WSL/macOS 环境安装 `tmux` 和 Cursor CLI。
2. 进入 `cursor-agent-system/`。
3. 用 `spawn-cursor.sh` 启动后台任务。
4. 用 `check-status.sh` 轮询状态。
5. 需要人工调整时，用 `send-command.sh` 发指令。
6. 需要进入现场时，用 `attach-session.sh` 附加。
7. 任务结束后，用 `kill-session.sh` 做优雅收口。


path: cursor-agent-system/templates/cursor-task-prompt.md
content: # OpenClaw Cursor 任务模板

将下面内容补全后，可直接作为 `spawn-cursor.sh --task-file` 的输入。

# OpenClaw Cursor 任务

## 基本信息
- 任务 ID: {{TASK_ID}}
- 会话名: {{SESSION_NAME}}
- 项目路径: {{PROJECT_PATH}}
- 创建时间: {{CREATED_AT}}
- 优先级: {{PRIORITY}}
- 预计耗时: {{ETA}}

## 任务描述
{{TASK_DESCRIPTION}}

## 上下文
- 当前项目技术栈: {{TECH_STACK}}
- 参考文件: {{REFERENCE_FILES}}
- 验收标准: {{ACCEPTANCE_CRITERIA}}

## 执行要求
1. 先阅读相关代码，理解现有架构。
2. 遵循项目规范和命名约定。
3. 对重要改动持续输出进度。
4. 运行必要测试并总结结果。
5. 如遇阻塞，明确说明原因和建议方案。

## 标准输出格式
开始任务时输出：
```
============================================================
开始执行: {{TASK_NAME}}
工作目录: {{PROJECT_PATH}}
预计耗时: {{ETA}}
============================================================
```

完成步骤时输出：
```
已完成步骤 N: [步骤名称]
耗时: [X 分钟]
产出: [简要说明]
```

任务完成时输出：
```
============================================================
任务完成: {{TASK_NAME}}
总耗时: [总时间]
完成清单:
- [子任务]
修改文件:
- [文件路径]
测试结果:
- [测试摘要]
备注:
- [补充说明]
============================================================
```

## 控制指令
- `/pause`
- `/resume`
- `/status`
- `/focus [子任务]`
- `/expand [新增需求]`
- `/abort [原因]`


path: cursor-agent-system/scripts/attach-session.sh
content: #!/usr/bin/env bash
# 附加到运行中的 tmux 会话

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./common.sh
source "${SCRIPT_DIR}/common.sh"

usage() {
  cat <<'EOF'
用法:
  ./scripts/attach-session.sh <会话名或任务别名>

说明:
  不带参数时列出所有已记录会话，方便你挑选后再附加。
EOF
}

ensure_runtime_dirs
require_command tmux "请先安装 tmux 3.3+。"

if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
  usage
  exit 0
fi

if [[ $# -eq 0 ]]; then
  print_title "可附加的会话"
  LIST_OUTPUT="$(list_known_sessions)"
  if [[ -z "${LIST_OUTPUT}" ]]; then
    info "当前没有已记录会话。"
    exit 0
  fi
  while IFS=$'\t' read -r session_name status task_name; do
    [[ -n "${session_name}" ]] || continue
    printf '%-38s %-12s %s\n' "${session_name}" "${status}" "${task_name}"
  done <<<"${LIST_OUTPUT}"
  printf '\n'
  info "请重新执行并传入具体会话名，例如: ./scripts/attach-session.sh myproj-auth-20260318-180000"
  exit 0
fi

SESSION_QUERY="$1"
SESSION_NAME="$(resolve_session_name "${SESSION_QUERY}")" || die "未找到匹配会话: ${SESSION_QUERY}"

session_exists "${SESSION_NAME}" || die "tmux 会话不存在或已结束: ${SESSION_NAME}"

print_title "附加到 tmux 会话"
printf '会话名称: %s\n' "${SESSION_NAME}"
printf '\n'
printf '%s\n' "分离会话请按: Ctrl+B, D"
printf '%s\n' "发送中断请按: Ctrl+C"
printf '%s\n' "直接退出 shell 会终止当前任务，请谨慎操作。"
printf '\n'

tmux attach -t "${SESSION_NAME}"


path: cursor-agent-system/scripts/check-status.sh
content: #!/usr/bin/env bash
# 检查 OpenClaw Cursor 后台任务状态

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./common.sh
source "${SCRIPT_DIR}/common.sh"

usage() {
  cat <<'EOF'
用法:
  ./scripts/check-status.sh [会话名或任务别名] [--json]

说明:
  不带参数时列出所有已记录任务。
  传入会话名时显示详细状态、PID、最近输出和状态文件内容。
EOF
}

infer_state_and_progress() {
  python3 - "$1" "$2" "$3" "$4" <<'PY'
import json
import re
import sys

existing = sys.argv[1]
captured = sys.argv[2]
session_alive = sys.argv[3] == "true"
pane_command = sys.argv[4].strip().lower()

state = existing or "unknown"
completed = []
current = ""
progress = "0%"

lines = [line.strip() for line in captured.splitlines() if line.strip()]

for line in lines:
    if re.match(r"^已完成步骤\s*\d+[:：]", line):
        completed.append(line)
    elif re.match(r"^(开始执行|任务完成|遇到问题)[:：]", line):
        current = line

last_line = lines[-1] if lines else ""
shell_commands = {"bash", "sh", "zsh", "dash", "fish"}
shell_idle = session_alive and pane_command in shell_commands and (
    last_line in {"$", "#"} or
    last_line.endswith("$") or
    last_line.endswith("#")
)

failure_markers = [
    "unexpected EOF",
    "command not found",
    "agent_exit_code=",
    "Workspace Trust Required",
    "No such file or directory",
]
failure_reason = ""
for marker in failure_markers:
    if marker in captured:
        failure_reason = marker
        break

json_result_success = '"type":"result","subtype":"success"' in captured
json_result_error = '"type":"result","subtype":"error"' in captured or '"is_error":true' in captured
json_tool_started = '"type":"tool_call","subtype":"started"' in captured
json_tool_completed = '"type":"tool_call","subtype":"completed"' in captured
json_assistant = '"type":"assistant"' in captured
json_thinking = '"type":"thinking"' in captured

if "任务完成:" in captured:
    state = "completed"
    progress = "100%"
    if not current:
        current = "任务已完成"
elif json_result_success:
    state = "completed"
    progress = "100%"
    current = current or "Cursor Agent 已返回成功结果"
elif json_result_error:
    state = "failed"
    progress = "0%"
    current = current or "Cursor Agent 返回错误结果"
elif "遇到问题:" in captured:
    state = "blocked"
    progress = "90%" if completed else "20%"
    if not current:
        current = "任务阻塞，等待处理"
elif json_tool_started or json_tool_completed or json_assistant or json_thinking:
    state = "running"
    if json_tool_started and not current:
        current = "Cursor 正在调用工具"
    elif json_assistant and not current:
        current = "Cursor 正在输出阶段性结果"
    elif json_thinking and not current:
        current = "Cursor 正在分析任务"
    progress = "35%" if json_tool_started else "15%"
elif shell_idle and ("agent --trust" in captured or "cursor-agent --trust" in captured):
    state = "failed"
    progress = "0%"
    if failure_reason == "unexpected EOF":
        current = "Cursor CLI 启动命令解析失败"
    elif failure_reason == "command not found":
        current = "运行环境缺少必要命令"
    elif failure_reason == "Workspace Trust Required":
        current = "Cursor CLI 卡在工作区信任确认"
    elif failure_reason.startswith("agent_exit_code="):
        current = "Cursor CLI 已退出"
    else:
        current = "Cursor CLI 已退出或未正确启动"
elif "开始执行:" in captured or completed:
    state = "running"
    current = current or (completed[-1] if completed else "任务运行中")
    estimated = min(95, max(10, len(completed) * 15))
    progress = f"{estimated}%"
elif not session_alive and state in {"starting", "running", "paused", "blocked", "unknown"}:
    state = "stopped"
    current = current or "tmux 会话已结束"
    progress = "100%" if existing == "completed" else progress

payload = {
    "status": state,
    "currentStep": current or "暂无明确进度输出",
    "completedSteps": completed,
    "estimatedProgress": progress,
}
print(json.dumps(payload, ensure_ascii=False))
PY
}

ensure_runtime_dirs
require_command python3 "请先安装 Python 3。"
require_command tmux "请先安装 tmux 3.3+。"

OUTPUT_JSON="false"
POSITIONAL=()

while [[ $# -gt 0 ]]; do
  case "$1" in
    --json)
      OUTPUT_JSON="true"
      shift
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      POSITIONAL+=("$1")
      shift
      ;;
  esac
done

set -- "${POSITIONAL[@]}"

if [[ $# -eq 0 ]]; then
  if [[ "${OUTPUT_JSON}" == "true" ]]; then
    python3 - "${STATUS_DIR}" <<'PY'
import json
import os
import sys

status_dir = sys.argv[1]
items = []
if os.path.isdir(status_dir):
    for name in os.listdir(status_dir):
        if not name.endswith(".json"):
            continue
        path = os.path.join(status_dir, name)
        try:
            with open(path, "r", encoding="utf-8") as fh:
                data = json.load(fh)
        except Exception:
            continue
        items.append({
            "sessionName": data.get("sessionName") or os.path.splitext(name)[0],
            "taskName": data.get("taskName"),
            "status": data.get("status"),
            "createdAt": data.get("createdAt"),
            "statusFile": path,
        })
print(json.dumps({"ok": True, "items": items}, ensure_ascii=False))
PY
    exit 0
  fi
  print_title "已记录的 Cursor 会话"
  if [[ ! -d "${STATUS_DIR}" ]]; then
    info "当前还没有状态文件。"
    exit 0
  fi

  LIST_OUTPUT="$(list_known_sessions)"
  if [[ -z "${LIST_OUTPUT}" ]]; then
    info "当前还没有状态文件。"
    exit 0
  fi

  printf '%-38s %-12s %s\n' "会话名" "状态" "任务标识"
  print_divider
  while IFS=$'\t' read -r session_name status task_name; do
    [[ -n "${session_name}" ]] || continue
    printf '%-38s %-12s %s\n' "${session_name}" "${status}" "${task_name}"
  done <<<"${LIST_OUTPUT}"
  exit 0
fi

SESSION_QUERY="$1"
SESSION_NAME="$(resolve_session_name "${SESSION_QUERY}")" || die "未找到匹配会话: ${SESSION_QUERY}"
STATUS_FILE="$(status_file_path "${SESSION_NAME}")"
[[ -f "${STATUS_FILE}" ]] || die "状态文件不存在: ${STATUS_FILE}"

LAST_CHECK_AT="$(now_iso)"
STATUS_JSON="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1], encoding="utf-8")).get("status","unknown"))' "${STATUS_FILE}")"
HAS_SESSION="false"
PID=""
PROCESS_INFO=""
LAST_CAPTURE=""
PANE_COMMAND=""
PANE_PATH=""

if session_exists "${SESSION_NAME}"; then
  HAS_SESSION="true"
  PID="$(pane_pid "${SESSION_NAME}")"
  PANE_COMMAND="$(pane_current_command "${SESSION_NAME}")"
  PANE_PATH="$(pane_current_path "${SESSION_NAME}")"
  LAST_CAPTURE="$(capture_recent_output "${SESSION_NAME}" 30)"
  if [[ -n "${PID}" ]]; then
    PROCESS_INFO="$(ps -p "${PID}" -o pid=,stat=,etime=,command= 2>/dev/null || true)"
  fi
else
  LAST_CAPTURE="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1], encoding="utf-8")).get("output",{}).get("lastCapture",""))' "${STATUS_FILE}")"
fi

INFERRED_JSON="$(infer_state_and_progress "${STATUS_JSON}" "${LAST_CAPTURE}" "${HAS_SESSION}" "${PANE_COMMAND}")"
LAST_CAPTURE_JSON="$(python3 -c 'import json,sys; print(json.dumps(sys.stdin.read(), ensure_ascii=False))' <<<"${LAST_CAPTURE}")"
if [[ -n "${PID}" ]]; then
  PID_UPDATE="${PID}"
else
  PID_UPDATE="null"
fi

update_status_file "${STATUS_FILE}" \
  "status=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["status"])' "${INFERRED_JSON}")" \
  "lastCheckAt=${LAST_CHECK_AT}" \
  "cursor.pid=${PID_UPDATE}" \
  "progress.currentStep=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["currentStep"])' "${INFERRED_JSON}")" \
  "progress.completedSteps=json:$(python3 -c 'import json,sys; print(json.dumps(json.loads(sys.argv[1])["completedSteps"], ensure_ascii=False))' "${INFERRED_JSON}")" \
  "progress.estimatedProgress=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["estimatedProgress"])' "${INFERRED_JSON}")" \
  "output.lastCapture=json:${LAST_CAPTURE_JSON}"

if [[ "${OUTPUT_JSON}" == "true" ]]; then
  python3 - "${STATUS_FILE}" "${HAS_SESSION}" "${PID}" "${PROCESS_INFO}" "${PANE_COMMAND}" "${PANE_PATH}" <<'PY'
import json
import sys

path = sys.argv[1]
has_session = sys.argv[2] == "true"
pid = sys.argv[3] or None
process_info = sys.argv[4]
pane_command = sys.argv[5]
pane_path = sys.argv[6]

with open(path, "r", encoding="utf-8") as fh:
    data = json.load(fh)

print(json.dumps({
    "ok": True,
    "sessionName": data.get("sessionName"),
    "statusFile": path,
    "hasSession": has_session,
    "pid": pid,
    "processInfo": process_info,
    "paneCommand": pane_command,
    "panePath": pane_path,
    "status": data,
}, ensure_ascii=False))
PY
  exit 0
fi

print_title "任务状态"
printf '查询输入:   %s\n' "${SESSION_QUERY}"
printf '会话名称:   %s\n' "${SESSION_NAME}"
printf '状态文件:   %s\n' "${STATUS_FILE}"
printf '最近检查:   %s\n' "${LAST_CHECK_AT}"
printf '\n'

if [[ "${HAS_SESSION}" == "true" ]]; then
  printf 'tmux 会话:   运行中\n'
  if [[ -n "${PID}" ]]; then
    printf 'Pane PID:    %s\n' "${PID}"
  fi
  if [[ -n "${PANE_COMMAND}" ]]; then
    printf 'Pane 命令:   %s\n' "${PANE_COMMAND}"
  fi
  if [[ -n "${PANE_PATH}" ]]; then
    printf 'Pane 路径:   %s\n' "${PANE_PATH}"
  fi
else
  printf 'tmux 会话:   未运行或已结束\n'
fi

if [[ -n "${PROCESS_INFO}" ]]; then
  printf '进程信息:    %s\n' "${PROCESS_INFO}"
fi

printf '\n状态内容:\n'
print_divider
pretty_print_json "${STATUS_FILE}"
print_divider

printf '\n最近输出 (最后 30 行):\n'
print_divider
if [[ -n "${LAST_CAPTURE}" ]]; then
  printf '%s\n' "${LAST_CAPTURE}"
else
  printf '%s\n' "(暂无输出)"
fi
print_divider


path: cursor-agent-system/scripts/common.sh
content: #!/usr/bin/env bash

set -euo pipefail

export PYTHONIOENCODING="${PYTHONIOENCODING:-utf-8:replace}"
export PYTHONUTF8="${PYTHONUTF8:-1}"
export LANG="${LANG:-C.UTF-8}"
export LC_ALL="${LC_ALL:-C.UTF-8}"

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SYSTEM_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
STATUS_DIR="${SYSTEM_ROOT}/status"
TASKS_DIR="${SYSTEM_ROOT}/tasks"
LOGS_DIR="${SYSTEM_ROOT}/logs"
TEMPLATES_DIR="${SYSTEM_ROOT}/templates"

print_divider() {
  printf '%s\n' "============================================================"
}

print_title() {
  print_divider
  printf '%s\n' "$1"
  print_divider
}

info() {
  printf '[信息] %s\n' "$*"
}

warn() {
  printf '[警告] %s\n' "$*" >&2
}

error() {
  printf '[错误] %s\n' "$*" >&2
}

die() {
  error "$*"
  exit 1
}

ensure_runtime_dirs() {
  mkdir -p "${STATUS_DIR}" "${TASKS_DIR}" "${LOGS_DIR}" "${TEMPLATES_DIR}"
}

require_command() {
  local command_name="$1"
  local help_text="${2:-}"
  if ! command -v "${command_name}" >/dev/null 2>&1; then
    if [[ -n "${help_text}" ]]; then
      die "缺少命令 ${command_name}。${help_text}"
    fi
    die "缺少命令 ${command_name}。"
  fi
}

detect_agent_command() {
  if command -v agent >/dev/null 2>&1; then
    printf '%s\n' "agent"
    return 0
  fi
  if command -v cursor-agent >/dev/null 2>&1; then
    printf '%s\n' "cursor-agent"
    return 0
  fi
  return 1
}

slugify() {
  python3 - "$1" <<'PY'
import re
import sys

value = sys.argv[1].strip().lower()
value = re.sub(r"[^a-z0-9]+", "-", value)
value = re.sub(r"-{2,}", "-", value).strip("-")
print(value or "task")
PY
}

now_iso() {
  python3 - <<'PY'
from datetime import datetime, timezone
print(datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"))
PY
}

timestamp_compact() {
  date +"%Y%m%d-%H%M%S"
}

status_file_path() {
  printf '%s\n' "${STATUS_DIR}/$1.json"
}

task_file_path() {
  printf '%s\n' "${TASKS_DIR}/$1.md"
}

runner_file_path() {
  printf '%s\n' "${TASKS_DIR}/$1.runner.sh"
}

log_file_path() {
  printf '%s\n' "${LOGS_DIR}/$1.log"
}

session_exists() {
  tmux has-session -t "$1" 2>/dev/null
}

pane_pid() {
  tmux display-message -p -t "$1" "#{pane_pid}" 2>/dev/null || true
}

pane_current_command() {
  tmux display-message -p -t "$1" "#{pane_current_command}" 2>/dev/null || true
}

pane_current_path() {
  tmux display-message -p -t "$1" "#{pane_current_path}" 2>/dev/null || true
}

capture_recent_output() {
  local session_name="$1"
  local lines="${2:-30}"
  tmux capture-pane -t "${session_name}" -p -S "-${lines}" 2>/dev/null || true
}

pretty_print_json() {
  python3 - "$1" <<'PY'
import json
import sys

path = sys.argv[1]
with open(path, "r", encoding="utf-8") as fh:
    data = json.load(fh)
print(json.dumps(data, ensure_ascii=False, indent=2))
PY
}

resolve_session_name() {
  local query="${1:-}"
  [[ -n "${query}" ]] || return 1

  python3 - "${STATUS_DIR}" "${query}" <<'PY'
import json
import os
import sys
from datetime import datetime

status_dir = sys.argv[1]
query = sys.argv[2].strip()

if not query or not os.path.isdir(status_dir):
    sys.exit(1)

def parse_time(value: str) -> datetime:
    if not value:
        return datetime.min
    try:
        return datetime.fromisoformat(value.replace("Z", "+00:00"))
    except Exception:
        return datetime.min

records = []
for name in os.listdir(status_dir):
    if not name.endswith(".json"):
        continue
    path = os.path.join(status_dir, name)
    try:
        with open(path, "r", encoding="utf-8") as fh:
            data = json.load(fh)
    except Exception:
        continue
    session_name = str(data.get("sessionName") or os.path.splitext(name)[0]).strip()
    task_name = str(data.get("taskName") or "").strip()
    created_at = str(data.get("createdAt") or "").strip()
    records.append({
        "session": session_name,
        "task": task_name,
        "createdAt": created_at,
    })

exact = [r for r in records if query == r["session"]]
if exact:
    exact.sort(key=lambda item: parse_time(item["createdAt"]), reverse=True)
    print(exact[0]["session"])
    sys.exit(0)

partial = [
    r for r in records
    if query in r["session"] or (r["task"] and query in r["task"])
]
if partial:
    partial.sort(key=lambda item: parse_time(item["createdAt"]), reverse=True)
    print(partial[0]["session"])
    sys.exit(0)

sys.exit(1)
PY
}

update_status_file() {
  local file_path="$1"
  shift

  python3 - "${file_path}" "$@" <<'PY'
import json
import os
import sys

path = sys.argv[1]
pairs = sys.argv[2:]

data = {}
if os.path.exists(path):
    try:
        with open(path, "r", encoding="utf-8") as fh:
            data = json.load(fh)
    except Exception:
        data = {}

def parse_value(raw: str):
    if raw.startswith("json:"):
        return json.loads(raw[5:])
    if raw == "null":
        return None
    if raw == "true":
        return True
    if raw == "false":
        return False
    return raw

for pair in pairs:
    key, value = pair.split("=", 1)
    current = data
    pieces = key.split(".")
    for part in pieces[:-1]:
        node = current.get(part)
        if not isinstance(node, dict):
          node = {}
          current[part] = node
        current = node
    current[pieces[-1]] = parse_value(value)

with open(path, "w", encoding="utf-8", newline="\n") as fh:
    json.dump(data, fh, ensure_ascii=False, indent=2)
    fh.write("\n")
PY
}

list_known_sessions() {
  python3 - "${STATUS_DIR}" <<'PY'
import json
import os
import sys

status_dir = sys.argv[1]
if not os.path.isdir(status_dir):
    sys.exit(0)

items = []
for name in os.listdir(status_dir):
    if not name.endswith(".json"):
        continue
    path = os.path.join(status_dir, name)
    try:
        with open(path, "r", encoding="utf-8") as fh:
            data = json.load(fh)
    except Exception:
        continue
    items.append((
        str(data.get("createdAt") or ""),
        str(data.get("sessionName") or os.path.splitext(name)[0]),
        str(data.get("status") or "unknown"),
        str(data.get("taskName") or ""),
    ))

for _, session_name, status, task_name in sorted(items, reverse=True):
    print(f"{session_name}\t{status}\t{task_name}")
PY
}


path: cursor-agent-system/scripts/kill-session.sh
content: #!/usr/bin/env bash
# 优雅或强制终止 Cursor 后台会话

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./common.sh
source "${SCRIPT_DIR}/common.sh"

usage() {
  cat <<'EOF'
用法:
  ./scripts/kill-session.sh <会话名或任务别名> [--force] [--yes] [--purge] [--json]

选项:
  --force   会话在优雅退出后仍存活时，直接 kill-session
  --yes     跳过确认提示
  --purge   终止后同时删除状态文件、任务文件和日志文件
EOF
}

ensure_runtime_dirs
require_command tmux "请先安装 tmux 3.3+。"
require_command python3 "请先安装 Python 3。"

FORCE="false"
ASSUME_YES="false"
PURGE="false"
OUTPUT_JSON="false"
POSITIONAL=()

while [[ $# -gt 0 ]]; do
  case "$1" in
    --force)
      FORCE="true"
      shift
      ;;
    --yes)
      ASSUME_YES="true"
      shift
      ;;
    --purge)
      PURGE="true"
      shift
      ;;
    --json)
      OUTPUT_JSON="true"
      shift
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      POSITIONAL+=("$1")
      shift
      ;;
  esac
done

set -- "${POSITIONAL[@]}"
[[ $# -ge 1 ]] || {
  usage
  exit 1
}

SESSION_QUERY="$1"
SESSION_NAME="$(resolve_session_name "${SESSION_QUERY}")" || die "未找到匹配会话: ${SESSION_QUERY}"
STATUS_FILE="$(status_file_path "${SESSION_NAME}")"
TASK_FILE="$(task_file_path "${SESSION_NAME}")"
LOG_FILE="$(log_file_path "${SESSION_NAME}")"

if [[ "${OUTPUT_JSON}" != "true" ]]; then
  print_title "终止 Cursor 会话"
  printf '会话名称: %s\n' "${SESSION_NAME}"
  printf '强制模式: %s\n' "${FORCE}"
  printf '清理文件: %s\n' "${PURGE}"
  printf '\n'
fi

if [[ "${ASSUME_YES}" != "true" && -t 0 ]]; then
  read -r -p "确认继续终止该会话吗？[y/N] " answer
  case "${answer}" in
    y|Y|yes|YES) ;;
    *) die "已取消终止操作。" ;;
  esac
fi

LAST_CAPTURE=""
if session_exists "${SESSION_NAME}"; then
  LAST_CAPTURE="$(capture_recent_output "${SESSION_NAME}" 30)"
  if [[ "${OUTPUT_JSON}" != "true" ]]; then
    info "先尝试优雅终止（发送 Ctrl+C）..."
  fi
  tmux send-keys -t "${SESSION_NAME}" C-c

  for _ in 1 2 3 4 5; do
    sleep 1
    if ! session_exists "${SESSION_NAME}"; then
      break
    fi
  done
fi

FINAL_STATUS="terminated"
if session_exists "${SESSION_NAME}"; then
  if [[ "${FORCE}" == "true" ]]; then
    if [[ "${OUTPUT_JSON}" != "true" ]]; then
      warn "会话仍未退出，执行强制终止。"
    fi
    tmux kill-session -t "${SESSION_NAME}"
    FINAL_STATUS="killed"
  else
    if [[ "${OUTPUT_JSON}" != "true" ]]; then
      warn "会话仍在运行。如需强制结束，请追加 --force。"
    fi
    FINAL_STATUS="running"
  fi
fi

LAST_CAPTURE_JSON="$(python3 -c 'import json,sys; print(json.dumps(sys.stdin.read(), ensure_ascii=False))' <<<"${LAST_CAPTURE}")"
STOPPED_AT="$(now_iso)"

if [[ -f "${STATUS_FILE}" ]]; then
  update_status_file "${STATUS_FILE}" \
    "status=${FINAL_STATUS}" \
    "stoppedAt=${STOPPED_AT}" \
    "lastCheckAt=${STOPPED_AT}" \
    "progress.currentStep=会话已停止" \
    "output.lastCapture=json:${LAST_CAPTURE_JSON}"
fi

if [[ "${PURGE}" == "true" ]]; then
  rm -f "${STATUS_FILE}" "${TASK_FILE}" "${LOG_FILE}"
  if [[ "${OUTPUT_JSON}" != "true" ]]; then
    info "已清理关联文件。"
  fi
fi

if [[ "${FINAL_STATUS}" == "running" ]]; then
  if [[ "${OUTPUT_JSON}" == "true" ]]; then
    printf '{"ok":false,"sessionName":"%s","status":"%s"}\n' "${SESSION_NAME}" "${FINAL_STATUS}"
  fi
  exit 1
fi

if [[ "${OUTPUT_JSON}" == "true" ]]; then
  python3 - "${STATUS_FILE}" "${SESSION_NAME}" "${FINAL_STATUS}" "${PURGE}" <<'PY'
import json
import os
import sys

status_file = sys.argv[1]
session_name = sys.argv[2]
final_status = sys.argv[3]
purged = sys.argv[4] == "true"
data = None
if os.path.exists(status_file):
    with open(status_file, "r", encoding="utf-8") as fh:
        data = json.load(fh)
print(json.dumps({
    "ok": True,
    "sessionName": session_name,
    "status": final_status,
    "purged": purged,
    "statusData": data,
}, ensure_ascii=False))
PY
else
  info "会话已经结束。"
fi


path: cursor-agent-system/scripts/send-command.sh
content: #!/usr/bin/env bash
# 向运行中的 Cursor 会话发送额外指令

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./common.sh
source "${SCRIPT_DIR}/common.sh"

usage() {
  cat <<'EOF'
用法:
  ./scripts/send-command.sh <会话名或任务别名> <指令> [--json]

特殊指令:
  /pause
  /resume
  /status
  /focus [子任务]
  /expand [新增需求]
  /abort [原因]
EOF
}

ensure_runtime_dirs
require_command tmux "请先安装 tmux 3.3+。"
require_command python3 "请先安装 Python 3。"

OUTPUT_JSON="false"
POSITIONAL=()

while [[ $# -gt 0 ]]; do
  case "$1" in
    --json)
      OUTPUT_JSON="true"
      shift
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      POSITIONAL+=("$1")
      shift
      ;;
  esac
done

set -- "${POSITIONAL[@]}"

[[ $# -ge 2 ]] || {
  usage
  exit 1
}

SESSION_QUERY="$1"
shift
COMMAND_TEXT="$*"
SESSION_NAME="$(resolve_session_name "${SESSION_QUERY}")" || die "未找到匹配会话: ${SESSION_QUERY}"
STATUS_FILE="$(status_file_path "${SESSION_NAME}")"

session_exists "${SESSION_NAME}" || die "tmux 会话不存在或已结束: ${SESSION_NAME}"

BEFORE_CAPTURE="$(capture_recent_output "${SESSION_NAME}" 10)"

if [[ "${OUTPUT_JSON}" != "true" ]]; then
  print_title "发送额外指令"
  printf '会话名称: %s\n' "${SESSION_NAME}"
  printf '发送内容: %s\n' "${COMMAND_TEXT}"
  printf '\n'
fi

tmux send-keys -t "${SESSION_NAME}" "${COMMAND_TEXT}" C-m
sleep 1

AFTER_CAPTURE="$(capture_recent_output "${SESSION_NAME}" 10)"
LAST_CAPTURE_JSON="$(python3 -c 'import json,sys; print(json.dumps(sys.stdin.read(), ensure_ascii=False))' <<<"${AFTER_CAPTURE}")"

DELIVERED="false"
if [[ "${AFTER_CAPTURE}" != "${BEFORE_CAPTURE}" ]] || [[ "${AFTER_CAPTURE}" == *"${COMMAND_TEXT}"* ]]; then
  DELIVERED="true"
fi

case "${COMMAND_TEXT}" in
  /pause*)
    update_status_file "${STATUS_FILE}" \
      "status=paused" \
      "progress.currentStep=已发送暂停指令，等待 Cursor 响应" \
      "lastCheckAt=$(now_iso)" \
      "output.lastCapture=json:${LAST_CAPTURE_JSON}"
    ;;
  /resume*)
    update_status_file "${STATUS_FILE}" \
      "status=running" \
      "progress.currentStep=已发送恢复指令，等待 Cursor 继续执行" \
      "lastCheckAt=$(now_iso)" \
      "output.lastCapture=json:${LAST_CAPTURE_JSON}"
    ;;
  /status*)
    update_status_file "${STATUS_FILE}" \
      "lastCheckAt=$(now_iso)" \
      "output.lastCapture=json:${LAST_CAPTURE_JSON}"
    ;;
  *)
    update_status_file "${STATUS_FILE}" \
      "status=running" \
      "progress.currentStep=已发送补充指令，等待 Cursor 消化新需求" \
      "lastCheckAt=$(now_iso)" \
      "output.lastCapture=json:${LAST_CAPTURE_JSON}"
    ;;
esac

if [[ "${OUTPUT_JSON}" == "true" ]]; then
  python3 - "${STATUS_FILE}" "${DELIVERED}" "${COMMAND_TEXT}" <<'PY'
import json
import sys

path = sys.argv[1]
delivered = sys.argv[2] == "true"
command = sys.argv[3]

with open(path, "r", encoding="utf-8") as fh:
    data = json.load(fh)

print(json.dumps({
    "ok": delivered,
    "delivered": delivered,
    "sessionName": data.get("sessionName"),
    "command": command,
    "status": data,
}, ensure_ascii=False))
PY
else
  if [[ "${DELIVERED}" == "true" ]]; then
    info "指令已写入 tmux 会话。"
  else
    warn "已尝试发送指令，但暂未在最近输出中观察到回显。"
  fi

  print_divider
  printf '最近输出:\n'
  print_divider
  printf '%s\n' "${AFTER_CAPTURE:-"(暂无输出)"}"
  print_divider
fi


path: cursor-agent-system/scripts/spawn-cursor.sh
content: #!/usr/bin/env bash
# 启动 OpenClaw -> tmux -> Cursor CLI 后台任务

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./common.sh
source "${SCRIPT_DIR}/common.sh"

usage() {
  cat <<'EOF'
用法:
  ./scripts/spawn-cursor.sh <任务标识> <任务描述> [项目路径] [--priority 优先级] [--eta 预计耗时] [--task-file 现成任务文件] [--json]

参数:
  任务标识            简短任务名，例如 feature-auth
  任务描述            要发给 Cursor CLI 的核心任务描述
  项目路径            可选，默认当前目录

选项:
  --priority          任务优先级，默认 normal
  --eta               预计耗时，默认 30-60分钟
  --task-file         使用现成 Markdown 任务文件，不再自动生成正文
  --json              只输出机器可读 JSON
  -h, --help          显示帮助

示例:
  ./scripts/spawn-cursor.sh feature-auth "实现 JWT 登录接口" /workspace/myapp --priority high --eta 45分钟
EOF
}

ensure_runtime_dirs

PRIORITY="normal"
ETA="30-60分钟"
EXTERNAL_TASK_FILE=""
OUTPUT_JSON="false"
POSITIONAL=()

while [[ $# -gt 0 ]]; do
  case "$1" in
    --priority)
      [[ $# -ge 2 ]] || die "--priority 需要一个值。"
      PRIORITY="$2"
      shift 2
      ;;
    --eta)
      [[ $# -ge 2 ]] || die "--eta 需要一个值。"
      ETA="$2"
      shift 2
      ;;
    --task-file)
      [[ $# -ge 2 ]] || die "--task-file 需要一个路径。"
      EXTERNAL_TASK_FILE="$2"
      shift 2
      ;;
    --json)
      OUTPUT_JSON="true"
      shift
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      POSITIONAL+=("$1")
      shift
      ;;
  esac
done

set -- "${POSITIONAL[@]}"

TASK_NAME="${1:-}"
TASK_DESCRIPTION="${2:-}"
PROJECT_PATH="${3:-$(pwd)}"
ORIGINAL_PROJECT_PATH="${PROJECT_PATH}"

[[ -n "${TASK_NAME}" && -n "${TASK_DESCRIPTION}" ]] || {
  usage
  exit 1
}

require_command tmux "请先安装 tmux 3.3+。"
require_command python3 "请先安装 Python 3。"
AGENT_BIN="$(detect_agent_command)" || die "未找到 Cursor CLI。请先安装 agent 或 cursor-agent。"

[[ -d "${PROJECT_PATH}" ]] || die "项目路径不存在: ${PROJECT_PATH}"

if command -v cygpath >/dev/null 2>&1; then
  case "${PROJECT_PATH}" in
    [A-Za-z]:[\\/]*)
      PROJECT_PATH="$(cygpath -u "${PROJECT_PATH}")"
      ;;
  esac
  if [[ -n "${EXTERNAL_TASK_FILE}" ]]; then
    case "${EXTERNAL_TASK_FILE}" in
      [A-Za-z]:[\\/]*)
        EXTERNAL_TASK_FILE="$(cygpath -u "${EXTERNAL_TASK_FILE}")"
        ;;
    esac
  fi
fi

WORKSPACE_PATH="${PROJECT_PATH}"
if command -v cygpath >/dev/null 2>&1; then
  WORKSPACE_PATH="$(cygpath -w "${PROJECT_PATH}" 2>/dev/null || printf '%s' "${ORIGINAL_PROJECT_PATH}")"
fi

TASK_SLUG="$(slugify "${TASK_NAME}")"
PROJECT_SLUG="$(slugify "$(basename "${PROJECT_PATH}")")"
TIMESTAMP="$(timestamp_compact)"
SESSION_NAME="${PROJECT_SLUG}-${TASK_SLUG}-${TIMESTAMP}"
TASK_ID="${SESSION_NAME}"
TASK_FILE="$(task_file_path "${SESSION_NAME}")"
RUNNER_FILE="$(runner_file_path "${SESSION_NAME}")"
STATUS_FILE="$(status_file_path "${SESSION_NAME}")"
LOG_FILE="$(log_file_path "${SESSION_NAME}")"
CREATED_AT="$(now_iso)"

if [[ "${OUTPUT_JSON}" != "true" ]]; then
  print_title "启动 Cursor 后台任务"
  printf '任务 ID:    %s\n' "${TASK_ID}"
  printf '会话名称:   %s\n' "${SESSION_NAME}"
  printf '项目路径:   %s\n' "${PROJECT_PATH}"
  printf '优先级:     %s\n' "${PRIORITY}"
  printf '预计耗时:   %s\n' "${ETA}"
  printf '任务文件:   %s\n' "${TASK_FILE}"
  printf '启动脚本:   %s\n' "${RUNNER_FILE}"
  printf '状态文件:   %s\n' "${STATUS_FILE}"
  printf '日志文件:   %s\n' "${LOG_FILE}"
  printf '\n'
fi

if [[ -n "${EXTERNAL_TASK_FILE}" ]]; then
  [[ -f "${EXTERNAL_TASK_FILE}" ]] || die "指定的任务文件不存在: ${EXTERNAL_TASK_FILE}"
  cp "${EXTERNAL_TASK_FILE}" "${TASK_FILE}"
  if [[ "${OUTPUT_JSON}" != "true" ]]; then
    info "已复制现成任务文件。"
  fi
else
  cat > "${TASK_FILE}" <<EOF
# OpenClaw Cursor 任务

## 基本信息
- 任务 ID: ${TASK_ID}
- 会话名: ${SESSION_NAME}
- 项目路径: ${PROJECT_PATH}
- 创建时间: ${CREATED_AT}
- 优先级: ${PRIORITY}
- 预计耗时: ${ETA}

## 任务描述
${TASK_DESCRIPTION}

## 角色
你是 Cursor CLI Agent，通过 tmux 会话接收来自 OpenClaw 的任务。请在开始编码前先阅读相关代码，再按现有架构实现需求。

## 执行要求
1. 先理解项目结构和已有实现，再开始修改。
2. 遵循现有命名、风格、测试和文档约定。
3. 修改前说明要做什么，执行过程中持续输出进度。
4. 遇到阻塞时明确说明问题、已尝试方案和需要的决策。
5. 完成后给出修改文件、测试结果和后续建议。

## 标准输出格式
开始任务时输出：
\`\`\`
============================================================
开始执行: ${TASK_NAME}
工作目录: ${PROJECT_PATH}
预计耗时: ${ETA}
============================================================
\`\`\`

完成步骤时输出：
\`\`\`
已完成步骤 N: [步骤名称]
耗时: [X 分钟]
产出: [简要说明]
\`\`\`

遇到问题时输出：
\`\`\`
遇到问题: [问题描述]
详情: [详细说明]
尝试方案:
1. [方案一]
2. [方案二]
建议: [需要用户决策的内容]
\`\`\`

任务完成时输出：
\`\`\`
============================================================
任务完成: ${TASK_NAME}
总耗时: [总时间]
完成清单:
- [子任务]
修改文件:
- [文件路径] (+新增/-删除)
测试结果:
- [测试摘要]
备注:
- [重要说明]
============================================================
\`\`\`

## 运行中可能收到的控制指令
- /pause: 暂停当前推进并进入等待输入状态
- /resume: 恢复推进
- /status: 输出当前步骤、已完成事项、剩余工作
- /focus [子任务]: 缩小范围，优先处理指定部分
- /expand [需求]: 接受新增范围并说明影响
- /abort [原因]: 保存当前状态并优雅退出
EOF
  if [[ "${OUTPUT_JSON}" != "true" ]]; then
    info "已生成标准任务文件。"
  fi
fi

cat > "${STATUS_FILE}" <<EOF
{
  "taskId": "${TASK_ID}",
  "taskName": "${TASK_NAME}",
  "sessionName": "${SESSION_NAME}",
  "status": "starting",
  "projectPath": "${PROJECT_PATH}",
  "priority": "${PRIORITY}",
  "estimatedDuration": "${ETA}",
  "createdAt": "${CREATED_AT}",
  "startedAt": "${CREATED_AT}",
  "lastCheckAt": "${CREATED_AT}",
  "taskFile": "${TASK_FILE}",
  "runnerFile": "${RUNNER_FILE}",
  "logFile": "${LOG_FILE}",
  "cursor": {
    "pid": null,
    "command": "${AGENT_BIN}",
    "exitCode": null
  },
  "progress": {
    "currentStep": "准备启动 Cursor CLI",
    "completedSteps": [],
    "estimatedProgress": "0%"
  },
  "output": {
    "lastCapture": "",
    "logFile": "${LOG_FILE}"
  }
}
EOF

cat > "${RUNNER_FILE}" <<EOF
#!/usr/bin/env bash
set -euo pipefail

if [ -f ~/.bashrc ]; then . ~/.bashrc; fi

export OPENCLAW_TASK_ID='${TASK_ID}'
export OPENCLAW_SESSION_NAME='${SESSION_NAME}'
export OPENCLAW_PRIORITY='${PRIORITY}'
export OPENCLAW_ESTIMATED_DURATION='${ETA}'
export PYTHONIOENCODING='utf-8'
export PYTHONUTF8='1'
export LANG='C.UTF-8'
export LC_ALL='C.UTF-8'
export OPENCLAW_WORKSPACE_PATH='${WORKSPACE_PATH}'

PROMPT_FILE='${TASK_FILE}'
PROMPT_CONTENT="\$(cat "\${PROMPT_FILE}")"

printf '\\n[openclaw] 使用任务文件: %s\\n' "\${PROMPT_FILE}"
set +e
'${AGENT_BIN}' --trust --force --print --output-format stream-json --stream-partial-output --workspace "\${OPENCLAW_WORKSPACE_PATH}" "\${PROMPT_CONTENT}"
AGENT_EXIT_CODE="\$?"
set -e
printf '[openclaw] agent_exit_code=%s\\n' "\${AGENT_EXIT_CODE}"
exit "\${AGENT_EXIT_CODE}"
EOF
chmod +x "${RUNNER_FILE}"

if [[ "${OUTPUT_JSON}" != "true" ]]; then
  info "正在创建 tmux 会话..."
fi
tmux new-session -d -s "${SESSION_NAME}" -c "${PROJECT_PATH}"
tmux pipe-pane -o -t "${SESSION_NAME}" "cat >> '${LOG_FILE}'"

printf -v RUNNER_COMMAND 'bash %q' "${RUNNER_FILE}"
tmux send-keys -t "${SESSION_NAME}" "${RUNNER_COMMAND}" C-m
sleep 1

PANE_PID="$(pane_pid "${SESSION_NAME}")"
LAST_CAPTURE="$(capture_recent_output "${SESSION_NAME}" 30)"
LAST_CAPTURE_JSON="$(python3 -c 'import json,sys; print(json.dumps(sys.stdin.read(), ensure_ascii=False))' <<<"${LAST_CAPTURE}")"
if [[ -n "${PANE_PID}" ]]; then
  PANE_PID_VALUE="${PANE_PID}"
else
  PANE_PID_VALUE="null"
fi

update_status_file "${STATUS_FILE}" \
  "status=running" \
  "cursor.pid=${PANE_PID_VALUE}" \
  "progress.currentStep=Cursor CLI 已启动，等待任务响应" \
  "progress.estimatedProgress=5%" \
  "lastCheckAt=$(now_iso)" \
  "output.lastCapture=json:${LAST_CAPTURE_JSON}"

if [[ "${OUTPUT_JSON}" == "true" ]]; then
  python3 - "${STATUS_FILE}" <<'PY'
import json
import sys

path = sys.argv[1]
with open(path, "r", encoding="utf-8") as fh:
    data = json.load(fh)
payload = {
    "ok": True,
    "taskId": data.get("taskId"),
    "taskName": data.get("taskName"),
    "sessionName": data.get("sessionName"),
    "status": data.get("status"),
    "projectPath": data.get("projectPath"),
    "priority": data.get("priority"),
    "estimatedDuration": data.get("estimatedDuration"),
    "taskFile": data.get("taskFile"),
    "statusFile": path,
    "logFile": data.get("logFile"),
    "cursor": data.get("cursor", {}),
}
print(json.dumps(payload, ensure_ascii=False))
PY
else
  printf '\n'
  info "任务已经在后台启动。"
  print_divider
  printf '检查状态: ./scripts/check-status.sh %s\n' "${SESSION_NAME}"
  printf '实时附加: ./scripts/attach-session.sh %s\n' "${SESSION_NAME}"
  printf '发送指令: ./scripts/send-command.sh %s "补充说明"\n' "${SESSION_NAME}"
  printf '终止会话: ./scripts/kill-session.sh %s\n' "${SESSION_NAME}"
  print_divider
fi


path: .cursor/skills/openclaw-cursor-agent-system/references/setup.md
content: # Setup Checklist

This reference defines the expected behavior for the `OpenClaw -> tmux -> Cursor CLI` architecture.

## Build Checklist

- Create `cursor-agent-system/` with `scripts/`, `templates/`, `status/`, `tasks/`, and `logs/`.
- Add a shared `scripts/common.sh` for path resolution, status updates, session lookup, and output capture.
- Keep each script focused on one lifecycle action.
- Make session names unique with `{project}-{task}-{timestamp}`.
- Store status, task, and log artifacts using the session name as the base file name.

## Script Behavior

### `spawn-cursor.sh`

- Validate `tmux`, Cursor CLI, and `python3`.
- Accept task name, task description, project path, priority, ETA, and optional existing task file.
- Create the task Markdown and initial status JSON when no external task file is supplied.
- Start a detached `tmux` session in the target project directory.
- Pipe pane output to the session log file.
- Launch Cursor CLI with the task file content inside the tmux session.

### `check-status.sh`

- Support two modes:
  - no argument: list all recorded sessions
  - one argument: resolve a session or task alias and print detailed status
- Show whether the tmux session exists.
- Show pane PID when available.
- Capture the latest output, ideally 30 lines.
- Update the status JSON with the latest snapshot.

### `attach-session.sh`

- Resolve a session alias to the latest matching session.
- Refuse to attach if the tmux session no longer exists.
- Print a short reminder for `Ctrl+B, D`.

### `send-command.sh`

- Resolve the session alias.
- Send an extra instruction through `tmux send-keys`.
- Support `/pause`, `/resume`, `/status`, `/focus`, `/expand`, and `/abort`.
- Update the status JSON after sending.

### `kill-session.sh`

- Ask for confirmation unless an explicit no-prompt flag is present.
- Try graceful termination with `Ctrl+C` first.
- If the session still exists, allow `--force` to kill it.
- Optionally support file cleanup, but do not purge by default.

## UX Requirements

- Use Chinese-facing messages for errors and operational hints.
- Include clear section dividers in command output.
- Keep exit codes meaningful: `0` for success, non-zero for failure.
- Avoid brittle session lookup patterns such as `find | grep | head`; use deterministic file naming or structured lookup instead.

## Validation

After implementation or edits, run:

```bash
bash -n scripts/common.sh scripts/spawn-cursor.sh scripts/check-status.sh scripts/attach-session.sh scripts/send-command.sh scripts/kill-session.sh
```

If the environment has `shellcheck`, run it as an extra pass.


path: .cursor/skills/openclaw-cursor-agent-system/references/task-prompt.md
content: # Task Prompt Structure

The canonical task prompt template is at `cursor-agent-system/templates/cursor-task-prompt.md`.

`spawn-cursor.sh` uses this template to auto-generate task files. Refer to the template for the full structure including placeholders for task name, description, project path, priority, and control instructions.


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)